Repository Health Checks: Hygiene Signals Behind Security Debt
A findings count tells you what is wrong in a repository today. It does not tell you why the same repository keeps producing findings, or whether anyone will fix the next one. That second question is what repository health answers.
Repository health is the set of conditions around the code: who owns it, whether the default branch is protected, whether changes get reviewed, whether a live credential sits in the history, and whether your security checks run there at all. None of these are vulnerabilities. All of them decide how fast vulnerabilities accumulate and how long they stay open.
Most AppSec programs measure the debt. Few measure the hygiene that generates it. This post walks through the signals that matter, what each one predicts, and how to turn a health score into a work queue instead of another dashboard.
What repository health measures, and what it does not
A repository health check asks a yes-or-no question about a control, not about a finding. Is there an owning team? Is the default branch protected? Are reviews required before merge?
The answers describe the repository's operating conditions, not its defects.
That is a different axis from severity. A repository can carry zero critical findings and still be unhealthy: no owner, no branch protection, no review requirement, no coverage. It is clean by luck. The next dependency bump or AI-generated change lands unreviewed, and nobody is on the hook when it goes wrong.
The reverse is also common. A repository with forty open findings but a named team, protected branches, required reviews and guardrails in place is in a better position than the clean one. The findings have a path to closure. Health measures whether that path exists.
Keep the two separate on purpose. Mixing finding counts into a health score produces a number that moves when someone closes tickets, which hides the fact that the conditions never changed.
The hygiene signals behind security debt: ownership, visibility, secrets
These are the checks worth putting in a repository health score. Each one maps to a specific failure mode you have probably already seen.
Ownership: team and owner
An unowned repository is the single most expensive hygiene gap, because every downstream process depends on it. Tickets have nowhere to route. SLOs have nobody to hold accountable. Notifications go to a channel nobody reads.
The finding is real and the fix is known, and it sits there anyway.
Two checks cover this: a team that owns the repository, and a named owner or lead. The team drives routing. The owner is who you call.
Visibility
Public and private repositories carry different risk for the same defect. A leaked token in a public repository is exposed to anyone with a search engine. A dangerous workflow in a public repository can be triggered by a fork. Visibility is not a control you fix, but it is a multiplier on every other check, and a repository that flipped from private to public without anyone noticing is a change you want recorded.
Validated secrets
A validated secret is a credential that still works. This is the one hygiene signal that is also an active incident. A repository with a live credential in it fails health regardless of how good its other controls are, because the credential is already outside the control boundary.
Validation matters here: unvalidated secret matches are mostly test fixtures and sample keys, and a health score built on raw matches will be ignored within a week. See secrets detection and validation for why that distinction is the whole game.
Branch protection, required reviews and coverage
Branch protection
Branch protection is what turns every other gate from advisory into enforced. Without it, a failing status check is a suggestion. A required review is optional. A direct push to the default branch is allowed.
GitHub documents the mechanism in About protected branches, and GitLab and Bitbucket have equivalents.
Required reviews
Required reviews are the control that makes code review a property of the repository rather than a habit of the team. A repository where the author can merge their own change with no second set of eyes has no review process, whatever the engineering handbook says. With AI coding agents opening pull requests, this check has gone from good practice to table stakes.
Guardrail coverage and workflow coverage
Coverage checks answer a question people assume is already yes: does security tooling actually run on this repository? A guardrail that evaluates pull requests, and a workflow that routes what the scans find, are what connect detection to response. A repository with neither is a repository where findings are discovered on a schedule and then wait.
Why ownership gaps turn into security debt
Most of the debt in a mature program is not in repositories nobody scans. It is in repositories everyone scans and nobody owns. The scan runs, the findings are created, the SLO clock starts, and nothing else happens.
The pattern is easy to recognize once you look for it. Pull the overdue findings, group them by repository, and check how many of those repositories have a team assigned. The unowned ones dominate the tail. They are usually old services, forks, internal tools, and anything created by someone who has since left.
This is why ownership belongs at the top of a repository health score and not in an appendix. Fixing ownership on twenty repositories does more for your overdue count than fixing twenty findings, because it gives every future finding in those repositories a path to closure.
Synced team sources help. GitHub teams, GitLab groups, CODEOWNERS files and service catalogs already know who owns what. GitHub's CODEOWNERS mechanism in particular ties ownership to paths, which is also what required reviews key on. The health check is what tells you where the sync has gaps.
Branch protection and required reviews: check them, do not assume them
Teams routinely believe their default branches are protected because a policy says so. The policy was applied to the repositories that existed when it was written. New repositories inherit whatever the creator clicked, which is usually nothing.
You can verify the actual state from the API. On GitHub, this returns the review requirement for a branch, or a 404 if the branch has no protection at all:
gh api repos/OWNER/REPO/branches/main/protection --jq '.required_pull_request_reviews.required_approving_review_count'
Run that across an organization and the answer is rarely uniform. Some branches require two approvals, some require one, some require none, and some return a 404 because protection was never turned on. Rulesets, which can apply at the organization level, change the picture again. The point of a health check is to stop asking people and start reading the configuration.
A few traps when scoring this signal:
- Admin bypass. Protection that administrators can bypass is weaker than it looks. Track bypass events separately, as a behavioral signal, rather than folding them into the health check.
- Protection on the wrong branch. Protecting
masteron a repository whose default branch is nowmainprotects nothing. Score the effective default branch. - Required checks that do not exist. A required status check named after a CI job that was renamed will block every merge, and the fix is usually to remove the requirement rather than restore the job. Health checks should confirm the required check is one that actually runs.
The OpenSSF Scorecard project scores the same two controls, Branch-Protection and Code-Review, on open-source projects, and its check definitions are a useful reference for what "protected" should mean in detail. Scorecard is built for open-source consumers judging maintainers. Repository health is the same question turned inward.
Reading a health score without gaming it
A score is only useful if the people who own the repositories believe it. Three design choices decide that.
Unknown is not a fail. Some checks cannot be measured for some sources. A source control system that does not expose review settings through its API cannot be scored on required reviews.
Marking that check unknown and leaving it out of the score is correct. Marking it failed punishes a team for a limitation of the integration, and they will stop looking at the number.
Every check needs a fix action. A red cell with no next step is a complaint. Each check should state what to change, where, and ideally offer the change directly: assign a team, open the branch settings, create the guardrail, rotate the secret.
Changes need a log. Health drifts. A branch protection that was on last quarter can be off today because someone needed to push a hotfix and never turned it back on. Record changes to the checks themselves, and to the properties around them: visibility, default branch, owners, teams. The log is what turns "the score dropped" into "this changed, on this date, and here is who to ask."
One more signal belongs beside the score even though it is not a check: when the repository was last analyzed. A health score computed from a scan that ran three months ago describes a repository that no longer exists. Sorting by last analyzed, with never-analyzed repositories at the bottom, is the fastest way to find the part of your estate the tooling has quietly stopped covering.
Using repository health as a work queue
The score is not the deliverable. The deliverable is a short list of repositories to fix this week. Here is an order that works.
- Validated secrets first. Any repository with a live credential is an incident, not a hygiene item. Rotate, then fix the health check.
- Unowned repositories in production. Filter to repositories with no team, intersect with anything deployed or tiered as critical, and assign owners. This is the highest-return hour an AppSec lead can spend.
- Unprotected default branches on owned, active repositories. Now that there is an owner, the request to turn on protection and required reviews has somewhere to go. Pair it with a guardrail in Observe mode so the team sees what the gate would have caught before it starts blocking. PR guardrails are designed for exactly that rollout path.
- Coverage gaps. Repositories with no guardrail or no workflow are where findings will be discovered late and routed nowhere. Fix the coverage before you argue about the findings.
- Archive the rest. A repository with no commits in a year, no owner and no deployments does not need branch protection. It needs to be archived so it stops diluting the score and stops holding credentials nobody remembers.
Report the score to leadership as a trend, by team, with the change log behind it. "Tier 1 repositories with branch protection and an owner went from 61 percent to 94 percent this quarter" is a sentence a CISO can use. A findings count rarely is. This is also how programs run AppSec at scale: fix the conditions once per repository instead of fixing findings one at a time forever.
How Heeler scores repository health
Catalog > Repositories in Heeler scores each repository on health checks for team, owner, visibility, validated secrets, branch protection, required reviews, guardrail coverage and workflow coverage. A check Heeler cannot measure for a source is marked unknown and left out of the score, so an integration limitation never reads as a failure.
The page opens on summary cards, view tabs, a health table and a drawer per repository with fix actions. A change log records changes to the checks and to visibility, default branch, secrets, owners and teams. Finding counts are not on this page by design: findings stay on the repository page and under Security, so the health view stays a view of conditions rather than a second findings dashboard. An optional, sortable Last Analyzed column shows when each repository was last fully analyzed, and repositories never fully analyzed sort last.
The surrounding pieces are already in the product. The Teams page carries a No team filter for closing ownership gaps, with teams synced from GitHub, GitLab or Port.io or created by hand. Each repository also carries an OSSF Scorecard tab with the full 17-check breakdown, covered in the earlier post on OSSF Scorecard for your own repositories.
Guardrails use Observe, Warn and Block actions, and Block holds a merge only when branch protection on the SCM side requires the check, which is one reason the branch protection health check sits next to guardrail coverage. Behavioral Checks raise a finding when a branch protection, ruleset or required status check is weakened or removed.
FAQ
What is a repository health check?
A repository health check is a yes-or-no test of a control around a repository, such as whether it has an owning team, a protected default branch, required reviews or security coverage, rather than a count of findings in it. Together the checks describe whether the repository can absorb and close findings, which is what a score summarizes.
Why separate repository health from finding counts?
Finding counts measure current debt; health measures the conditions that generate it. A score that mixes the two moves when tickets are closed even if nothing structural changed, so you lose the signal that tells you where the next findings will come from.
Which repository health check should an AppSec team fix first?
Validated secrets, then ownership. A live credential is an incident and gets rotated immediately; an unowned repository blocks every other fix because tickets, SLOs and notifications have nowhere to go.
Does branch protection alone make a repository healthy?
No. Branch protection makes other gates enforceable, but it needs required reviews, an owner, and a guardrail or check that actually runs to do any work. Protection with admin bypass and no required checks is close to no protection.
How should unknown checks be scored?
Leave them out of the score. A check that cannot be measured for a source is a limitation of the integration, not a failure of the team, and scoring it as a fail is the fastest way to get the score ignored.
Heeler scores every repository on the hygiene signals behind your security debt, with fix actions and a change log, in the same Catalog that holds your findings, dependencies and endpoints. Get a demo


.jpg)

