Repository navigation
v0.4.0
Infrastructure posture findings from Bicep, ARM and Dockerfiles were failing
builds. advisory_domains is what separates "this code is compromised" from
"a posture choice this project made about its own infrastructure", and it is
read from a rule's threat domain. The Azure and Dockerfile rule families had no
entry in that table, so they inherited a default of malware and scanner --
and sourceAddressPrefix: '*' in a Bicep file stopped a pipeline while the
identical finding in Terraform did not. A build that fails today on one of
those rules will pass after upgrading, which is what the README's own table
always said should happen.
Known-vulnerability detection worked for two ecosystems and not for the other
three. The OSV importer read only ECOSYSTEM-typed version ranges, and npm,
crates.io and Go publish almost entirely as SEMVER -- 215,140 of 229,191 npm
records, 2,827 of 2,844 crates.io, 9,260 of 9,305 Go. Everything else about
those advisories was correct, and none of them reached the database: npm shipped
397 vulnerability records beside 23,550 malicious ones, cargo shipped 37 and
gomod 112. A lockfile pinning lodash@4.17.15, axios@0.21.0,
minimist@1.2.0, smallvec@0.6.13 and github.com/gogo/protobuf@v1.3.1 --
44 real advisories between them -- reported nothing at all, while the Maven and
PyPI pins beside them reported 71 findings. The scan said complete: true and
gave no reason to doubt it.
Both range types are now read, and a range describing several disjoint intervals
is no longer folded into one that spans the gap between them. The database went
from 59,982 records to 268,578 across eleven ecosystems, and got smaller: it
ships gzipped, 36 MB to 7.2 MB. cargo 49 -> 964, gomod 112 -> 3,677,
npm 23,947 -> 227,937.
The [attest] extra reported every honest publisher as a forgery.
Verification called verify_artifact, which requires a bundle carrying a
messageSignature; npm --provenance and PyPI PEP 740 both publish DSSE
envelopes, so every genuine attestation was rejected with "Missing bundle
message signature" and reported at CRITICAL as a failed verification.
sigstore@4.1.0 on npm and sigstore==4.5.0 on PyPI -- published by the
Sigstore project itself -- were both accused. DSSE envelopes now go through
verify_dsse, and because that call proves who signed the envelope and nothing
about which artefact the statement describes, the in-toto subject digest is
compared against the pinned one: a genuine attestation for a different release
is still INVALID.
--online failed every Yarn Berry build. Berry's checksum: is a digest of
Yarn's own cache entry, prefixed with the cache key, and it was compared against
the tarball hash npm publishes -- which it can never equal. An untouched,
correct lockfile produced one CRITICAL "lockfile hash disagrees with the
registry" per dependency, up to the query ceiling, with the remediation "Do not
install". Only a value that resolves to a known algorithm and a digest of that
algorithm's length is compared now, and comparison is per algorithm, so a
lockfile recording npm's sha1 shasum no longer contradicts its sha512
integrity.
An advisory identifier is not unique, and the database was deduplicated as
though it were. One GHSA names several packages -- every tensorflow
advisory also names tensorflow-gpu and tensorflow-cpu, and 223 PyPI
identifiers name more than one package -- and one GHSA splits into several
records when the affected set is several disjoint windows, which is how Django's
212 records carry 80 identifiers. Collapsing on (ecosystem, identifier) threw
away 15,999 of 275,076 records: tensorflow-gpu 2.5.0 reported nothing while
tensorflow 2.5.0 reported 67, and 132 of Django's records never loaded. The
collision key now includes the package name, and only the hand-curated entries
suppress a generated one.
A pin that spells a release differently matched nothing. An enumerated
advisory lists versions as the upstream feed spells them -- OSV names Django's
release 3.2 -- and they were compared to the lockfile's string. pip install django==3.2.0 installs that same release, and django==3.2.0 in a
requirements.txt reported none of the twelve advisories django==3.2
reported, with the scan complete either way. Enumerated versions now go through
the same per-ecosystem comparator the ranges use.
The whole Swift feed was unreachable. OSV identifies a Swift package by its
clone URL (github.com/apple/swift-nio) and a Package.resolved by its
repository path (apple/swift-nio), so all 39 shipped Swift advisories were
loaded, indexed and matched by nothing.
One lockfile spoke for every ecosystem in its directory. Coverage was
recorded per project path, so a requirements.txt beside a conanfile.txt and
an environment.yml produced a graph of the Python pins alone -- the C++ and
conda dependencies were dropped entirely, and nothing said so.
Added
-
A reference on every rule the tool can emit, and a message on the 99 that had
none.cordon rules showprinted a title and a remediation and nothing that
explained the claim, andreferencesdid not exist on a detector rule at all
-- so the one place a reader goes to ask "says who?" answered for neither kind
of rule.core/references.pyholds the vocabulary: 45 URLs owned by a
standards body or by the vendor whose surface the rule is about. The 1,081
infrastructure policies take theirs from their control family, because the
family is the claim. Four rules carry none and say why: they report on the
scan itself, and there is no standards page for "the lookup failed". -
POLICY.DEPENDENCY.SECURITY_PLACEHOLDER.001, for a dependency pinned to
npm's-securityplaceholder. See Changed. -
Negation in a composite rule's terms.
unlessspeaks about paths, and
SUSPECT.DECODE_EXEC.001needed to say "a process start, but not the shell's". -
Two output formats for somebody else's tooling.
codeclimateis GitLab
Code Quality, the only report GitLab renders inline on every plan -- its
security dashboards are a licensed feature and this is not.vexis
CycloneDX VEX: an SBOM says what is in the artefact, and a VEX says which of
its vulnerabilities apply. With--reachability, a transitive dependency
first-party code does not import is recorded asnot_affectedwith the
justificationcode_not_reachable, so a consumer's tooling can drop what has
already been ruled out instead of raising it again. -
Infrastructure policy evaluated per resource. A new detector reads a
Terraform, Kubernetes, CloudFormation or Compose file into blocks and asks two
questions inside each one: does it say something insecure, and does it fail to
say something it must. The second is most of what infrastructure policy is
about and no file-level pattern can express it --storage_encryptedabsent
from anaws_db_instanceis an unencrypted database, written nowhere.
1,032 policies ship across Terraform, Kubernetes, CloudFormation, Compose
and Dockerfiles, each with the block it must report and the block it must not,
both run by the suite on every push. -
Most of those policies are generated from the providers' own schemas.
scripts/build_iac_policies.pyreadsterraform providers schema -jsonand
the AWS CloudFormation resource specification, and emits one policy per
resource that actually declares the attribute a control is about -- 3,845
resources between four providers. Which resources havestorage_encryptedis
a fact rather than a memory, and a policy naming an attribute a provider does
not have can never fire: it looks exactly like a clean scan. The set ships
gzipped beside the advisory data with a sidecar recording the provider
versions it was built from, and it is read one resource kind at a time, so a
repository with no infrastructure in it builds none of it. -
Nine more CI/CD rules, across four systems. A fork's pull request on a
self-hosted runner,workflow_runchecking out the commit that triggered it,
a publishing workflow restoring a cache a pull request can write,
permissions: write-all, and a reusable workflow called by a mutable ref --
plus the script-injection each of GitLab, Azure Pipelines, CircleCI and
Jenkins has, none of which had a rule of its own before. -
Six more ecosystems: Swift, Hex, CRAN, Conan, conda and Bazel, with the
three manifest shapes that were unreadable before them -- a Maven POM's own
coordinates and property substitution, an npm entry whose version is
inherited, and a hoisted entry that is not a direct dependency. -
Advisory feeds for Swift and Hex, taking the database to 268,578 records
from eleven ecosystems. CRAN was requested too and OSV's export produced
nothing this build keeps, so it is not shipped and not claimed: an empty file
is indistinguishable from a populated one at every layer above it. -
OPERATIONAL.ADVISORY.NO_FEED.001. Cordon reads seventeen ecosystems and
eleven have advisory records; a scan that includes a Conan, conda, Bazel or
CocoaPods dependency now says which of them could not be checked at all,
and clearscompleterather than ending in "0 findings, scan complete". -
A popular-package set and an allowlist for every ecosystem. Eight of the
seventeen had no popular set, so_typosquat_targetreturned before comparing
anything and the check silently did not run; six had no allowlist, which is
the state that makes a real package reportable as a squat of a name it
resembles. Both now ship for all seventeen. -
docs/07-ECOSYSTEMS.md: every ecosystem, the manifests and lockfiles read for
it, and which of advisories, typosquat, registry and provenance reach it.
Generated from the registry and asserted by the suite, like the coverage
matrix -- the README named no ecosystem at all before it. -
A live-registry gate on the release,
.github/workflows/live-checks.yml,
covering the defects an offline suite cannot see: an attestation verifier that
rejects every genuine bundle, a hash comparison that fires on every correct
Yarn Berry lockfile. -
Six more tutorials (06-08 and 12, 16, 17), covering secrets and exfiltration,
CI/CD attacks, containers and IaC, vetting a package before installing it, the
sandbox, and the source/build/binary domains. The set is renumbered so the
filenames follow the reading order, and the index, the chain ofNext:links
and the numbering are now asserted by the suite. -
OPERATIONAL.ADVISORY.DATABASE_SCOPEon every scan of a filtered database.
The bundled set is malicious plus high/critical, and that was previously said
only inside the staleness note -- so a database that was filtered and current,
which is every database for the weeks after a release, said nothing. -
OPERATIONAL.REGISTRY.NOT_ASKED.001and
OPERATIONAL.PROVENANCE.NOT_CHECKED.001for dependencies past a query ceiling
or a time budget. A 250-dependency lockfile reported "200 could not be
checked" and nothing about the other 50. -
Finding.degrades_coverage, which lets a detector clearScanResult.complete
for a limit only it can see. An--onlinescan whose entire network layer
failed used to reportcomplete: true. -
CycloneDX
hashesandlicenses, and SPDXchecksums,licenseConcluded,
licenseDeclaredandcopyrightText. The data was already parsed from the
lockfile and never emitted, and SPDX 2.3 requires the licence fields.
Fixed
-
A second-stage loader written on Windows was invisible. The pattern for an
interpreter reading its program from a pipe anchored on$, which under
MULTILINEmatches before a newline and not before the carriage return a CRLF
file puts in front of it. Finding out why exposed the larger gap: the execute
primitive knewsh -cand not the pipe that does the same thing with the
program arriving on stdin, soecho "$b" | base64 -d | bashwas only ever
reported because a command substitution elsewhere in the file counted as
starting a process. -
SUSPECT.DECODE_EXEC.001no longer treats a shell command substitution as
evidence that a decoded value was executed.key=$(echo "$b" | base64 -d)is
how a shell script decodes anything at all. -
SUSPECT.OBFUSCATION.LONGLINE.001requires the long line to contain an
unbroken run. It is about a payload kept off-screen in a diff, and a payload
is one token; a CloudFormationDashboardBodyand a Kubernetes CRD
description are long lines made of words. -
POLICY.COMPOSE.andPOLICY.CFN.sat beneathPOLICY.in the
attack-category table and could never match, reportingpolicywhere they
saidmisconfiguration. Three further entries were exact duplicates. -
Every entry in an npm v2/v3 lockfile was treated as a direct dependency. The
root entry records what the project actually asked for, and npm hoists a
transitive package to the top level where it looks identical -- so nothing was
ever transitive, and--reachability, which only lowers a transitive finding,
could never lower anything. -
A Maven POM's own
groupId/artifactIdwere not read, and${property}
references in a dependency's version were left unresolved, so a POM using the
ordinary${spring.version}idiom produced dependencies with no usable
version. -
An npm entry whose version comes from its parent was treated as unpinned, and
a hoisted entry innode_modules/was reported as a direct dependency. -
Two SBOMs of the same graph were not the same document: component ordering
and the generated serial number varied per run, so a diff of two exports of
an unchanged project was noise. -
The advisory database was built in full on every invocation -- thirteen files,
275,076 records, 1.34s -- before the walker read anything, and whether or not
the target had a single dependency. It is read per ecosystem now, on the first
question about that ecosystem, and the records are built into objects only for
the package names actually asked about. Scanning one file with no manifest
went from 1.81s to 0.44s, which is the difference between a pre-commit hook
people keep and one they pass--no-verifyto. -
RULEPACK_VERSIONsaid0.2.0while every bundled pack declared0.1.0and
nothing read the constant, so the text report, the SARIFproperties.rulepack
and the audit record all printed a number the release notes did not use. The
packs carry the documented version and the suite asserts they agree. -
uv.lockandpdm.lockwere declared supported and parsed as poetry
lockfiles. Theirdependenciesis an array, not a table, so the first package
raisedAttributeErrorand the whole file was lost -- no graph, no advisory
match, no SBOM for any project using either resolver. -
pnpm lockfile version 5 produced an empty graph, silently. The key is
/name/versionthere andname@versionin 6 and 9, and splitting on the last
@resolved every v5 key to an empty name. Keys that cannot be read now raise
rather than emptying the graph. -
--timeoutdid not bound the online phase. 80 dependencies under
--timeout 5took 24 seconds and still reportedcomplete: true; the
deadline now reaches both network detectors, and the provenance detector is
capped like the registry one rather than querying every dependency. -
Scanning a package archive skipped the dependency graph entirely, so the
question most people open a.tgzto ask -- is this a known-malicious
release? -- was the one it could not answer. -
SUSPECT.CRYPTOMINER.001fired at HIGH on any file naming the stratum
protocol, including a URL-parser test and this project's own advisory data. -
The sandbox reported "runtime is rootless" based on which binary was on PATH.
Podman runs rootful and Docker supports rootless; both are now asked. -
SARIF
uriBaseIdnamed a base the document never declared, andruleIndex
fell back to0, which points at a different rule. -
A PyPI filename's version was matched by substring, so a pin on
1.2matched
foo-1.2.3.tar.gz.
Changed
-
Infrastructure findings point at the line the attribute is on.
Block.bodyis
a slice of the file for Terraform and Bicep and the match offset is relative
to it, but it was added to the block header's offset -- so everyforbid
finding in those formats was out by the length of its own header.
publicNetworkAccess: 'Enabled'on line 20 was reported on line 15. Evidence
now carries a span as well. -
A dependency pinned to npm's
-securityplaceholder is reported at medium as
a withdrawn name, not at critical as a known-malicious release. npm publishes
that placeholder when its security team takes a name over, so it is the state
a project is in after the incident was dealt with; the advisory covers the
whole package and matched it too. -
Four rules were narrowed to what they can actually see, measured against
thirteen reference infrastructure repositories (20,310 files). An Azure
security rule now needs Allow, Inbound and an administrative destination port
beforesourceAddressPrefix: '*'is an opening -- it was reporting Deny and
Outbound rules.${{ toJSON(secrets) }}is critical when it reaches a
command's environment and high when it is passed to a reusable workflow, which
is how a composite action is given a credential at all. Four Kubernetes
hardening policies report at low when the flag is absent rather than wrong.
aws_ssm_parameteris asked for a KMS key when it holds aSecureString.
Findings over those repositories went from 2,722 to 2,560. -
Development Status :: 4 - Beta. Fourth release, seventeen ecosystems, a full
OSV-derived advisory layer, a documented interface split and 5,000 tests on
every push --3 - Alphawas inherited from 0.1.0 rather than decided. -
CI runs the suite with
-n autoand measures coverage on one job instead of
nine, and every job caches its wheels. The same matrix, the same tests: 26
minutes of wall clock to single digits. The suite itself went from 224s to 50s
on four cores, because everyEngineused to build and index the whole
advisory database and now indexes what it is asked about.