How CodeControl actually works
This is the long version, written for the person who will be asked whether the tool is any good — a developer, an agency lead, a CTO, or the technically-minded owner doing the evaluation. It covers what is checked, where each verdict comes from, how deep the coverage goes per ecosystem, and where the boundaries are. Nothing here is aspirational; it describes the software composition analysis (SCA) that is running today.
Looking for the plain-language version? CodeControl in business terms
What happens between a push and a finding
A scan resolves the dependency set, enriches every entry, matches it against live intelligence, and lands as a single roll-up observation that fans out into one finding per affected item. Findings are derived, never stored twice, and each dimension resolves independently as it clears.
express › body-parser › qs — so every transitive finding can say how the package got there. Development-only dependencies are identified from where the manifests state it and report one severity notch calmer, with the reason in the finding text.Four triggers, because they answer different questions
Almost everything a scan reports changes only when the code changes — dependencies, licences, secrets, base images, pipeline configuration. Exploitation is the exception: a dependency can enter CISA's known-exploited catalogue overnight with nothing committed. That asymmetry is why the schedule is split rather than uniform.
Full scan · every 14 days
Every dimension, from scratch. Staggered across targets so they don't all fire on the same date.
Advisory re-check · nightly
Vulnerability and exploitation data refreshed against the known dependency set. Everything outside the vulnerability picture is carried forward verbatim from the previous roll-up — a partial roll-up would otherwise resolve every secret and licence finding as "fixed".
Push to the default branch · within a minute
Delivered by webhook, so a credential committed at 14:00 is reported at 14:01. A push landing on an already-queued scan is swallowed rather than queued twice.
Pull request · a check run on the diff
The same secret detection, over only the lines the PR adds, posted back as a GitHub check run with per-line annotations — while the fix is still a branch rewrite rather than a credential rotation.
Thirteen dimensions and where each verdict comes from
Each dimension has its own section on the finding detail page and three tiers of guidance: technical detail, a how-to with worked examples, and a plain-language explanation. Every one of them can resolve on its own.
| Dimension | What it finds | Source of truth |
|---|---|---|
| Known vulnerabilities | Packages with published security advisories, across the full transitive tree | OSV.dev |
| Exploitation ranking | Which of those are confirmed exploited, or statistically likely to be next | CISA KEV + FIRST EPSS |
| Malicious packages | Packages published to steal credentials or install a backdoor | OpenSSF malicious-package advisories via OSV |
| Secrets in the code | API keys, payment credentials, database passwords in the working tree | In-memory tarball scan, ~45 issuer signatures |
| Secrets in git history | Credentials deleted from the code but still recoverable | Recent-commit diff sweep, disjoint from the above |
| Outdated / end of life | Packages far behind, or past their supported life | 8 package registries + endoflife.date |
| Unmaintained | Dependencies the author has deprecated, abandoned or stopped touching | npm deprecated / Packagist abandoned / staleness |
| Licence risk | Licences that can legally reach your own product, e.g. strong copyleft | SBOM licence verdicts + registry fallback |
| Typosquatting | Package names one edit away from a popular package | Offline Damerau-Levenshtein against popular lists |
| Package reputation | Brand-new packages — the slopsquatting attack aimed at AI-suggested names | Registry creation date (npm / PyPI / Packagist) |
| Dependency confusion | Internal package names that anyone could claim on a public registry | Public-registry existence check |
| Container base images | Operating-system images past end of life | Dockerfile / compose + endoflife.date |
| CI/CD pipeline | Weaknesses in the automation that holds your deployment credentials | .github/workflows audit |
Two facts travel on every dependency and change how all of the above read: the chain that pulled it in, and whether it is development-only.
The dimensions that are easy to build badly
Several of these are easy to implement badly and hard to implement usefully. This is what the difference consists of.
Exploitation ranking turns a list into a queue
Severity says how bad a flaw would be. KEV says exploitation has been observed. EPSS gives a 30-day probability for the long tail KEV doesn't cover. Three tiers — exploited in the wild, likely target (EPSS ≥ 0.5), elevated risk (≥ 0.10) — and the vulnerability table sorts by exploitation first, severity second. The finding itself stays strict, so it never cries wolf.
Secret scanning: two disjoint passes, plus evasion
The working tree is streamed as a tarball and parsed entirely in memory. History sweeps recent commit diffs and reports only credentials the working tree no longer contains — so the two findings never overlap, and the history one is exactly the "we deleted the line" class of leak. A third pass catches keys assembled from adjacent string literals or wrapped in base64; both run signature rules only, never entropy, because reconstructed text is random-looking for innocent reasons.
The value is never retained
A secret finding carries a file, a line, the detector that matched and a masked preview — sk_l…(28 chars). The raw credential is not stored, not logged, and not included in anything posted back to GitHub. The same discipline applies to pull-request check annotations.
Package reputation, aimed at slopsquatting
AI assistants confidently import packages that don't exist; attackers register exactly those plausible names, and the assistants go on recommending them. Such a package is not one edit from anything popular, so typosquatting is blind to it, and on day one no advisory names it, so the malware feed is too. The one thing it cannot fake is a history — so age is the only trigger: a direct dependency first published within 90 days. Install scripts, a missing source repository and a lone maintainer are aggravators reported on a young package, never findings against an established one. It auto-resolves once the package ages out.
Dependency confusion needs no onboarding
A direct dependency whose name returns 404 from its public registry is the exposure, regardless of who owns it — an attacker can publish that name and your next build may take theirs. A private registry configured in the repository is the attack's precondition and raises confidence; npm workspaces are detected so the innocent monorepo explanation is offered rather than buried.
CI/CD graded by mutability
A workflow runs with the repository's secrets and a write token, so a weakness there reaches the deployment path itself (see GitHub Actions security). Unpinned actions are graded by how mutable the reference is — third-party branch is high, third-party tag medium, a first-party tag low — because grading first-party tags higher would light up every repository on GitHub for following GitHub's own documentation. pull_request_target combined with a checkout of the PR head is treated as critical.
Ecosystem coverage, stated at the level it's actually true
Coverage is not uniform, and a scanner that implies otherwise is misleading you. Natively parsed means authoritative: exact pins, and a real direct/transitive split where the format has one.
package-lock.json, yarn.lock (classic and Berry), requirements.txt, poetry.lock, Pipfile.lock, composer.lock, go.mod, pom.xml, Gemfile.lock, Cargo.lock. Customer manifests are untrusted input: XML parsing refuses DOCTYPE outright, which closes XXE at the root, and there is a test that tries to read /etc/passwd through it.gradle.lockfile first (a real lockfile — exact versions, transitives included), then gradle/libs.versions.toml, then the literal coordinates in build.gradle(.kts), following settings.gradle includes. A version written as a variable, a + or a range stays blank — the build would have to run to know it — and Gradle stays flagged as a build we may not have read completely, because turning "we could not read this" into a clean bill of health is the failure mode worth avoiding.pom.xml is a manifest, not a lockfile: in the Java world it usually states none of its dependencies' versions — they come from a parent. We follow the same path Maven does: the POM's own dependency management, up the parent chain, then into imported BOMs. Multi-module builds are followed breadth-first with properties inherited down the branch, because an aggregator's root POM often declares no dependencies at all and reading it alone reports the repository as empty. ${project.version} is scoped to the POM that writes it — sharing one property map let a BOM inherit the customer's project version and resolve a package to a release that does not exist. A fabricated version is worse than a blank one: blank is reported as unchecked, while a wrong version is queried against advisories as fact.requirements.txt pins only what the author listed; pip resolves the rest at install time and writes no lockfile, so most of a Python project's real surface would simply be absent — neither reported risky nor reported unchecked. We walk the declared requirements breadth-first, choosing the newest stable release satisfying each PEP 440 specifier. Every such row is marked computed — a strong estimate of what a fresh install produces, never the pinned fact a lockfile states — and a constraint that cannot be satisfied resolves to nothing, counted and reported rather than guessed. A repository with a real Python lockfile is untouched.SBOM and VEX, generated from the scan you already ran
Served as a file download, because standard tooling expects the bare document rather than a wrapper.
SPDX 2.3
The format procurement, auditors and regulation ask for by name. An unrecognised licence is emitted as NOASSERTION rather than a guess — downstream tooling treats an SBOM as fact, so a guess propagates as one.
CycloneDX 1.5
What security tooling reads, and it has a native place for vulnerabilities — so the export carries what we know about the inventory, not just the inventory. Both formats are generated from the same scan, so they agree by construction.
VEX — the analysis half
An SBOM says what the software contains; a VEX says what its supplier has decided about each known vulnerability. The acknowledge-with-reason workflow has been collecting exactly that, so status maps to a CycloneDX analysis state with the human's own note attached. An advisory nobody has judged yet is stated as in triage, never omitted — a VEX listing only the flattering entries is worth nothing.
NIS2 already requires supply-chain security from companies in scope, and their suppliers feel it as a questionnaire. The Cyber Resilience Act phases bill-of-materials obligations in across 2026–27. This is the cheapest way to have an answer ready.
What we're granted, and what we do with it
Read-only contents, per repository
A per-tenant GitHub App, created and installed in one click, granted read access to repository contents — the minimum GitHub offers — on repositories you select individually. It cannot write code, open pull requests or change settings. The pull-request check adds exactly two permissions: checks:write to post the verdict and pull_requests:read to fetch the diff. Both are pinned by a test, so a wider grant cannot slip into the manifest unnoticed.
Source code is analysed in memory
The repository is streamed as a tarball and parsed in memory. Customer source is never written to disk and never stored. What persists is the derived inventory and the findings.
The webhook verifies before it reads
It is the platform's only unauthenticated write path, and every decision follows from that. The HMAC is verified over the raw request body before a single field is parsed — re-serialising a parsed object changes byte order and fails every verification — with a constant-time compare. No secret configured means a hard failure, never "accept": an instance that cannot verify must not act on unverified claims, including the claim that a customer uninstalled.
Identity comes from our own records
The tenant is resolved from the installation id looked up in our database, never from the repository name in the payload. A valid signature proves GitHub sent the message; it does not prove the sender may act on a given tenant's data. Zero or multiple matches are dropped rather than guessed.
Failure is never a green check
A pull-request diff we cannot fetch posts no check at all — a success the scanner did not earn is worse than silence. A check we cannot post says exactly that, so the operator can grant the permission rather than wonder.
Silent-failure detection
One webhook secret serves every installation, so a mismatch fails every customer at once with nothing but rejections in a log nobody reads. Consecutive rejections are the signal — the counter resets on any accepted delivery, so probing never accumulates into a false alarm — and a run of them is escalated.
Accuracy is measured on both sides of a deploy
This product is used by people who will believe everything it says — or stop believing all of it at the first error they happen to catch. So precision and recall are measured, not asserted.
Before release: a corpus gate in every build
Every engine dimension runs offline against a planted repository whose expected findings are written down by hand in an answer key. The assertion is exact set equality in both directions: a missing entry is a recall regression — a guard grown wide enough to swallow a real finding, which once cost eight seeded secrets in a single change — and an extra entry is a precision regression. Neither ships. The answer key is the specification, not a mirror of the code; a rule that legitimately changes behaviour updates the key in the same commit, with the reason.
The traps are the interesting part
Planted beside the real findings is every false positive we've learned from in production: environment-variable references versus defaults with a real value in them, documented example keys, placeholder passwords in Danish, SHA-pinned actions, Dockerfile stage aliases, the YAML on: keyword that parses as a boolean. Designing the corpus found a real bug on its own — an end-of-life rule that never fired for Django spelled with its conventional capital D.
After release: false positives are counted
"Not a real finding" is its own action, deliberately distinct from acknowledging. Acknowledging is a risk decision about your system; a false-positive report is evidence about our detection quality. It's counted per rule and surfaced as a detection-quality card, so a noisy detector is visible to us before it burns your trust.
End-to-end fixtures with written answer keys
Two private repositories back the offline gate: one with 108 planted items across ten dimensions, built deliberately hard — synthetic values with correct provider shapes and no placeholder vocabulary, so our own filters can't mask a miss — and one Maven aggregator whose root POM declares none of its five dependencies. Both found real bugs no unit test had.
Four rules the code actually follows
These aren't aspirations. Each one exists because of a specific way of being wrong that we'd rather not repeat.
What CodeControl does not do
Stated here rather than discovered later, because a buyer who finds a limit on their own stops trusting everything else on the page.
It reads what the repository declares
Not what the build produces. For most projects those are the same thing. Where a build computes its dependencies dynamically, some are out of reach — and the report says so rather than quietly showing fewer.
No analysis of your own code
There is no static analysis of hand-written application code, and that's deliberate. For this audience the code you wrote is a small fraction of what you actually run, and rule-based code analysis is the most false-positive-prone discipline in security. The precision of everything else is worth more than a noisy extra dimension.
Not every ecosystem is equally deep
Java, JavaScript, PHP, Python, Go, Ruby, Rust and .NET are covered well. Something more exotic depends on what GitHub itself can resolve — and the scan reports which parts it could not reach.
Not a penetration test, not a guarantee
It reports what is knowable from the repository. It cannot see how the application behaves in production, and it does not attempt to exploit anything. For the outside view of a running site, see how VulnControl scans a running site
Secret verification is deliberately not built yet
Checking whether a found key is still live with its provider means transmitting the customer's raw credential outward. That has to be designed together with the data-handling review — opt-in per tenant, read-only endpoints, never persisted — not bolted on because it demos well.
Pricing isn't published yet
The product is built and running; the commercial decision is the outstanding piece, and it's enabled per customer by hand today. Talk to us and we'll be straight about where that stands
Evaluate it against what you're using now
If you already run a supply-chain scanner, the useful comparison is the ranking, the dependency chains and the coverage reporting. Connect one repository and put them side by side.