capdrift 0.2.1 copy "capdrift: ^0.2.1" to clipboard
capdrift: ^0.2.1 copied to clipboard

Show what a Dart package can do, what changed between versions, and whether that change violates your policy.

Changelog #

0.2.1 #

Fixed #

  • The baseline hand-built its YAML, so a quote or a backslash in a qualifier corrupted the approval record. " ended the scalar early and left the file unparseable; \ was read as an escape introducer, so the ordinary Windows path c:\temp came back as c:<tab>emp and a fingerprint silently meant something else. Every quoted string is now produced by the JSON encoder, whose output is a valid YAML scalar. The fix is at the sink rather than in the fingerprint's own escaping, because a fingerprint travels to baselines, SARIF and CI output, and widening the value for whichever sink was noticed last is what produced this class of bug twice already. (#65)
  • The GitHub Action wrote untrusted report text into $GITHUB_OUTPUT behind a fixed delimiter. Capability qualifiers are lifted straight out of the analysed package's source, so a package containing a multi-line string literal could end the heredoc early and append its own records. The fail-on step reads decision back out, so a package could have written decision=ALLOW and walked through a gate meant to stop it. It is never executed; being read was enough. The delimiter is now random per run and checked against the payload. (#58)
  • A fingerprint could contain a newline. _escape covered % and : and nothing else, and the baseline writes each fingerprint as a quoted YAML scalar. A newline ended that scalar and the rest parsed as more YAML, so a package could inject entries into the file recording which of its own capabilities a human had approved. Newline and carriage return are now escaped, and the round trip is tested against a qualifier holding every escaped character. (#63)
  • The job summary wrapped report text in a fixed markdown fence. A qualifier carrying a backtick run closed it early and the remainder rendered as markdown, letting a package forge headings in the summary of any build that audited it. The fence is now widened past the longest run in the content. (#59)
  • Dependency graph edges dropped every non-hosted entry. Path and git dependencies vanished from the graph, so a local package looked like a leaf and anything below it was attributed to whoever else reached it. Whether a package can be fetched decides if it can be analysed; it never decided whether an edge exists. (#60)
  • Attribution ran over incomplete graphs. Git, SDK and custom-registry entries never reach the analysed set, so a package reachable only through one of them looked unreachable from the runtime roots and was reported dev-only. Attribution now declines when any source is unsupported, which is what the rest of the tool already does with an analysis hole. (#61)
  • A direct dev path dependency was audited without --include-dev. The path branch recorded the package before testing the flag while the hosted branch tested first, so the report said "runtime dependencies only" over a tree that contained a dev dependency. (#62)
  • The dynamic detector matched any identifier called Isolate. No import gate and no shadow check, unlike the ffi and mirrors detectors beside it. A package declaring its own Isolate produced a false finding, which is the exact behaviour this tool reads the AST to avoid. It now reuses the declaresOwnClass helper the process detector already had. (#57)
  • --format sarif silently emitted text for audit and diff. A CI job could ask for SARIF, upload nothing usable, and report success. Those commands now exit 3 and say SARIF is inspect only. Real tree-level SARIF needs package-scoped identity and is tracked in #56.

0.2.0 #

Added #

  • dart run tool/check.dart. One command runs format, analyze, the unit suite and the publish dry run, and exits non-zero if any of them fail. Answering "is this releasable" previously meant running four commands and reading four outputs, and reading output is where mistakes happen: dart test prints a stack-trace hint on some passing runs and dart analyze prints an issue count that says nothing about severity. Both were misread during development. A single exit code cannot be.
  • Path dependencies are analysed from disk. They were reported as unanalysable, which was correct when nothing read them and backwards as a permanent position: a path dependency is on the machine, and internal code never went through pub.dev review at all. Refusing to read a team's own packages while analysing everything they got from strangers was the wrong way round. The never-execute rule is unchanged: read and parse, never run. A missing directory is INCOMPLETE naming the path. Git and SDK entries remain unanalysable, because there is genuinely nothing there to read.
  • Transitive dependencies are attributed to runtime or dev. A lockfile records that a package is transitive but not which root pulled it in, so the report used to decline to answer. Every archive is already downloaded and each carries its own pubspec, so the dependency graph was sitting in hand: it now walks from the direct runtime dependencies, and anything the walk does not reach arrived through a dev dependency. A package reachable both ways counts as runtime, because that is what ships. Attribution is still declined when a package could not be analysed, since a graph with a hole in it cannot be walked past.
  • A GitHub Action. Adopting capdrift in CI is now one uses: line instead of an install step, a run step and exit-code handling. It pins the capdrift version by default so a new release cannot silently change a build's result, writes findings to the job summary rather than burying them in the log, and maps the four decisions onto a pass or fail through fail-on.
  • --format sarif. Findings can be uploaded to GitHub code scanning and appear in the Security tab and on the pull request diff, next to CodeQL, rather than only in a CI log. Execution surface maps to severity and a policy violation is always an error. The CF1 fingerprint is used as SARIF's partialFingerprints, so GitHub deduplicates findings across runs where line numbers moved.
  • Archive caching. audit re-downloaded every archive on every run, which was the main reason not to put it in CI. Measured on a 13-package tree: 1451ms cold, 113ms warm. Keyed on package and version, which is immutable on pub.dev, and cached bytes are verified against the lockfile hash where one is pinned so a poisoned entry costs a refetch rather than a wrong answer. --no-cache and --clear-cache are available.
  • Policy now applies to diff. capdrift.yaml was honoured by inspect and audit and silently ignored by the flagship command, so a rule denying subprocess execution in build hooks would not fire on an upgrade that introduced one. diff blocks only on violations the upgrade introduces: a standing violation is not news, and reporting it would make every diff of an already-non-compliant package look like a regression.
  • Dynamic code loading is now detected. This was a complete blind spot: a package calling Isolate.spawnUri downloads Dart source from a URI and runs it, and capdrift reported such a package as having no capabilities at all. Now covered: Isolate.spawnUri, Isolate.spawn, dart:mirrors, and deferred imports with their loadLibrary calls.
  • A DYNAMIC capability family, appended after the original seven. The family ids are a wire format that appears in committed baselines, so it was added rather than inserted and no existing id moved.
  • dart:mirrors is reported with a caveat rather than as an ordinary finding. Reflection lets a package build calls at runtime that no AST walk can see, so its presence means the rest of the profile may be incomplete, and that is a statement about the analysis rather than about the package.

Fixed #

  • --version reported 0.1.0 on a 0.1.2 package. The constant was hand-maintained and missed two releases. It also fed SARIF's tool.driver.version, so uploaded findings would have been attributed to a version that had not been current since the first publish. A test now fails when the constant and pubspec.yaml disagree.

Measured #

Corpus B recall is 11/11, up from 8/8, with a fixture per new detector.

Corpus A moved from 185 findings to 195, and consumer-facing from 12 to 19. That increase is signal rather than noise: build_runner genuinely calls Isolate.spawnUri, dio and analyzer use Isolate.spawn, and two test suites import dart:mirrors. Anyone depending on a code generator should know it can load arbitrary Dart.

0.1.2 #

Fixed #

  • audit --resolve silently dropped git, path and custom-hosted dependencies. 0.1.1 fixed this for the default lockfile path, and the resolver path kept doing it, and did it worse: it never mentioned the dropped dependency at all. Map constraints were discarded before resolution, so nothing downstream knew they existed, ALLOW was reachable, and --resolve --approve could write a baseline over a tree with a dependency nobody had looked at.

Added #

  • A coverage invariant test. Every audit path is now checked against one rule: if any dependency was not analysed, ALLOW is impossible. This class of bug appeared four separate times, and point fixes did not stop it recurring because each new path had to rediscover the rule. A future path that does not honour it fails the test.

Changed #

  • The --include-dev documentation said dev dependencies are excluded by default. What is excluded is direct dev dependencies; their transitives are still analysed, because a lockfile does not record which root introduced a transitive. The behaviour errs toward analysing more than promised, which is the safe direction, and the wording now says so.
  • The baseline refusal message leads with the reason and lists each unanalysable dependency with its source, instead of a package count that was often zero.

0.1.1 #

Security hardening. A second external review of the published 0.1.0 found five issues; every one was verified against the code before being fixed.

Fixed #

  • A custom hosted registry was fetched from pub.dev. source: hosted does not mean pub.dev, and a lockfile entry pointing at a private registry was downloaded from the public one. If a public package shared the name and version, capdrift analysed a different package and said nothing. Such entries are now reported as unsupported, with the registry named.
  • The archive is verified against the lockfile SHA-256. Previously we fetched by name and version and analysed whatever came back. A mismatch is now INCOMPLETE, naming both hashes, because we cannot say what a package does when the bytes are not the ones that were pinned.
  • The download itself is bounded. 0.1.0 bounded decompression but not the download, and http.get buffers a whole response before anything downstream can object. Downloads are streamed and aborted past 50 MB compressed.
  • The 30 second analysis limit is a real ceiling. It was checked between files, so one pathological parse could run indefinitely while SECURITY.md promised otherwise. Each package now analyses in a worker isolate that is killed when the deadline passes.
  • SECURITY.md contradicted itself, still describing dart pub get as what audit runs by default. It reads pubspec.lock and runs nothing.
  • audit returned ALLOW on a tree it had not fully analysed. A git, path or custom-registry dependency was named as skipped and the run still passed, so the report contradicted its own exit code. Worse, --approve would write a baseline over that partial tree, and every later drift check would measure against it. An unanalysable dependency now makes the audit REVIEW, or INCOMPLETE under incomplete_analysis: fail, and --approve refuses. The report names each one with its real source rather than a generic label.

Changed #

  • crypto added as a direct dependency for SHA-256. It was already in the tree transitively, so no new download.

0.1.0 #

First release.

Added #

  • capdrift diff <package> <from> <to>: reports capability changes between two published versions. Nothing else in the Dart ecosystem does this.
  • capdrift inspect <package> [version]: capability profile of one version.
  • capdrift audit [directory]: resolves the full dependency tree and reports what every package in it can do, separating capabilities you chose from ones you inherited.
  • capdrift audit --include-dev: audits dev_dependencies as well. They never ship to your users, but they run on your machine and in CI with your credentials in the environment, and the report labels them dev only so a reader can tell which findings reach production.
  • Scheduled corpus refresh workflow. The verify reference set is a snapshot, and an ageing set means verify quietly stops recognising near-misses of packages that became popular since. It regenerates monthly and opens a pull request only when the set actually changed.
  • capdrift audit --approve and capdrift audit --baseline: approve the current capability state once, then report only drift from it. Drift confined to test and example surfaces is shown but exits 0.
  • capdrift verify <package>: four checks for a name an assistant may have invented. Existence, then length-scaled Damerau-Levenshtein against a verified reference set of 162 popular packages, then adoption, then age against adoption. Similarity alone never warns.
  • Seven capability families: network, process execution, filesystem, environment, native code, build hooks, platform.
  • Platform capability detection: Android manifest permissions, iOS usage description keys, and the platforms a plugin ships native code for.
  • Execution surface model. A finding in hook/ outranks the same finding in test/, and surface drives both report ordering and policy scoping. A native surface ranks above lib, because a plugin's manifest reaches every consumer while the Kotlin or Swift beside it cannot be read.
  • Dependency resolution through dart pub get --no-precompile in a temporary directory with a temporary PUB_CACHE. It never writes into your project.
  • Stable capability fingerprints (CF1:FAMILY:SURFACE:API:QUALIFIER), which are independent of file position so reformatting produces an empty diff.
  • Surface-scoped policy in capdrift.yaml, with mandatory reasons on every rule.
  • Four exit codes: 0 ALLOW, 1 REVIEW, 2 BLOCK, 3 INCOMPLETE.
  • Text and versioned JSON output.

Verified #

  • Never-execute guarantee, enforced by a test that analyses a hostile fixture and asserts both that it did not run and that it was actually detected.
  • Corpus A: 15 popular packages, 185 findings, 12 consumer facing.
  • Corpus B: recall 8/8 against synthetic malicious fixtures, including a guard that an example app manifest never reports at native weight.
  • Corpus C: no phantom changes across real version pairs.

Fixed before first release #

Found by an external technical review, and each one verified against the code rather than taken on faith before being fixed.

  • audit reported on a tree you may not have. It resolved fresh from the constraints instead of reading your committed pubspec.lock, so a project locked at http 1.5.0 was audited at 1.6.0. It now reads the lockfile by default, with --resolve for the deliberate "what would I get" question.
  • Decompression was unbounded. The size check ran on the compressed bytes and again only after the archive had fully expanded. Measured: 299 KB of input materialised 300 MB before the limit fired. Decompression is now aborted mid-stream by a bounded sink, and stops at the limit.
  • The documented 30 second analysis budget was never enforced. maxAnalysisTime was declared and read nowhere. It is now a real deadline producing INCOMPLETE.
  • Baseline drift reported phantom removals. A package whose archive could not be fetched has an empty fingerprint set, which compared against a baseline read as every capability removed. Comparison now uses the same guard as approval.
  • SECURITY.md overstated the network claim. dart pub get performs its own downloads, so "every network call goes through PubClient" was false.

Known limits #

  • Transitive attribution needs every package to analyse. If one archive cannot be read it contributes no edges, so the graph has a hole and attribution is declined for that run rather than guessed.
  • Path, git and SDK dependencies cannot be analysed: they have no pub.dev archive. They are named in the report with their source, and their presence makes ALLOW impossible and prevents baseline approval, so a partially readable tree can never pass.
0
likes
160
points
92
downloads

Documentation

API reference

Publisher

verified publishertisankan.dev

Weekly Downloads

Show what a Dart package can do, what changed between versions, and whether that change violates your policy.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

analyzer, archive, args, crypto, http, path, pub_semver, yaml

More

Packages that depend on capdrift