Skip to content

v0.4.0

Choose a tag to compare

@Threx-code Threx-code released this 23 Sep 07:02
· 1973 commits to main since this release

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 show printed a title and a remediation and nothing that
    explained the claim, and references did 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.py holds 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 -security placeholder. See Changed.

  • Negation in a composite rule's terms. unless speaks about paths, and
    SUSPECT.DECODE_EXEC.001 needed to say "a process start, but not the shell's".

  • Two output formats for somebody else's tooling. codeclimate is GitLab
    Code Quality, the only report GitLab renders inline on every plan -- its
    security dashboards are a licensed feature and this is not. vex is
    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 as not_affected with the
    justification code_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_encrypted absent
    from an aws_db_instance is 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.py reads terraform providers schema -json and
    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 have storage_encrypted is
    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_run checking 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 clears complete rather 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_target returned 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 of Next: links
    and the numbering are now asserted by the suite.

  • OPERATIONAL.ADVISORY.DATABASE_SCOPE on 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.001 and
    OPERATIONAL.PROVENANCE.NOT_CHECKED.001 for 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 clear ScanResult.complete
    for a limit only it can see. An --online scan whose entire network layer
    failed used to report complete: true.

  • CycloneDX hashes and licenses, and SPDX checksums, licenseConcluded,
    licenseDeclared and copyrightText. 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
    MULTILINE matches 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 knew sh -c and not the pipe that does the same thing with the
    program arriving on stdin, so echo "$b" | base64 -d | bash was only ever
    reported because a command substitution elsewhere in the file counted as
    starting a process.

  • SUSPECT.DECODE_EXEC.001 no 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.001 requires 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 CloudFormation DashboardBody and a Kubernetes CRD
    description are long lines made of words.

  • POLICY.COMPOSE. and POLICY.CFN. sat beneath POLICY. in the
    attack-category table and could never match, reporting policy where they
    said misconfiguration. 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/artifactId were 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 in node_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-verify to.

  • RULEPACK_VERSION said 0.2.0 while every bundled pack declared 0.1.0 and
    nothing read the constant, so the text report, the SARIF properties.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.lock and pdm.lock were declared supported and parsed as poetry
    lockfiles. Their dependencies is an array, not a table, so the first package
    raised AttributeError and 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/version there and name@version in 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.

  • --timeout did not bound the online phase. 80 dependencies under
    --timeout 5 took 24 seconds and still reported complete: 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 .tgz to ask -- is this a known-malicious
    release? -- was the one it could not answer.

  • SUSPECT.CRYPTOMINER.001 fired 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 uriBaseId named a base the document never declared, and ruleIndex
    fell back to 0, which points at a different rule.

  • A PyPI filename's version was matched by substring, so a pin on 1.2 matched
    foo-1.2.3.tar.gz.

Changed

  • Infrastructure findings point at the line the attribute is on. Block.body is
    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 every forbid
    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 -security placeholder 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
    before sourceAddressPrefix: '*' 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_parameter is asked for a KMS key when it holds a SecureString.
    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 - Alpha was inherited from 0.1.0 rather than decided.

  • CI runs the suite with -n auto and 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 every Engine used to build and index the whole
    advisory database and now indexes what it is asked about.