Changelog¶
All notable changes to the Drift Detector plugin. Dates are YYYY-MM-DD.
Unreleased¶
Added¶
- Hardcoded-credential detection, as a fourth deterministic finding kind alongside CVE/EOL/
sunset. A pinned
gitleaksbinary scans each repo's full git history (not just the current tree) for known credential shapes, the same registry-first/GitHub-fallback/ hash-verified acquisitionast-grepalready used. Findings land in the Supply Chain plane with their own tile and a distinct urgency glyph (ranked above sunset/EOL — a live leak outranks a future migration deadline), through the identicalkind-tagged schema every other finding already uses — no new pipeline, no fourth plane. The matched secret value is never carried past detection: a finding says where a credential is (repo, file, line, commit), never repeats the live value intodrift.jsonor a filed issue, which would just create a second copy of the leak. Zero AI involvement in detection — this is Tier 0, strictly more deterministic than the OSV/EOL tiers, since it needs no network call at scan time at all.
v1.1.0 — 2026-09-03¶
Added¶
- The scanner says when it is out of date, on every run — not only in
doctor.doctorhas been able to report a stale install since 1.0.0, and nothing ever runsdoctor: the guided flow does not call it, so a stale install stayed invisible exactly where it mattered, inside the scan whose behaviour it shapes. This release is the worked example — a 1.0.0 install would have gone on hitting the leads-gate bug fixed below while believing itself current. The check now runs once per invocation, before the subcommand, and is built to be the quietest guard in the tree: it speaks only when the install is behind (current, unparseable and offline are all silence, because a line on every run trains people to ignore the one that matters), writes to stderr only — stdout carriesdrift.json, the SBOM andchat-summary, which consumers pipe — caches the answer for a day so a 52-repo fleet scan makes at most one request rather than 52, and can neither raise nor change an exit code. SetDRIFT_NO_UPDATE_CHECK=1to switch it off for air-gapped CI and byte-reproducibility runs; an always-on check with no off switch is one that gets disabled by deleting the code. It fetches withcurland falls back tourllib— on evidence, not taste: urllib was observed dying in the TLS handshake against the very URL curl fetched with a 200 in the same second, andbin/drift-scan's doctor block has always used curl for it. -
Two runtime rules for the AI plane, pinned by polarity-aware contract tests. A gate refusal is now reported and never worked around — the observed 2026-09-02 run met a refusal by editing the evidence and resubmitting until it passed, turning a real API version into
dated (see file:line)and shipping a lead less truthful than the one refused. The rule also closes the softer form: offering to fix and resubmit, framed as help, is the same workaround wearing a question mark, and "a new submission with its own evidence" now carries an operational test — going back to the source and reading it again, never retyping the JSON with the refused field blanked. Separately, the AI wait takes the progress cadence a fleet scan already has, now with a real[n/N] repos cross-checkedsignal (N is known because step 1 dispatches one agent per repo) and a stop condition. -
A vendor-coverage digest for management (
drift-scan coverage-report). The scanner has always known which detected vendors nobody has audited; it had no way to say so to anyone who does not readdrift.json. The digest states how many of the detected vendors are settled, how many call-sites sit behind the ones that are not, what moved since the previous scan, and which human sign-offs are about to lapse. It is a projection ofdrift.json— a newverifyinvariant (check_digest_matches_coverage) re-parses the rendered document and refuses one whose figures disagree with the report, because a digest gets mailed to people who will never open the report it summarises. No email is sent: the scanner renders the file and CI delivers it, so no SMTP credential or network dependency enters the scan path. INTERNALandACCEPTED— two human-signed terminal dispositions. Two cases can never be settled by reading a vendor's page: a library built in-house, and a vendor that publishes nothing findable. Both previously satUNAUDITEDforever, keeping a work-order permanently non-empty with tasks that can never succeed — the defectBLOCKEDfixed one case earlier. They are deliberately two verdicts:INTERNALsettles a vendor because in-house code has no external lifecycle, whileACCEPTEDnames a risk without measuring it and so keeps counting toward unaudited exposure exactly asBLOCKEDdoes. Collapsing them would render a live exposure identically to a resolved one.- A named approver, enforced. A disposition is refused unless it carries an approver name,
a role, a substantive basis and a future expiry date. Expiry is mandatory and lapses the
vendor back to
UNAUDITED: a signed judgement may persist, but not forever and not silently. LikeBLOCKED, the payload nests under its verdict key so a scanner predating these verdicts reads the entry asUNAUDITEDrather thanCURRENT— data ships ahead of the code that understands it, and an unknown verdict must fail toward under-claiming. The flat form is refused outright rather than ignored. -
Run-over-run catalog movement (
catalogDeltaindrift.json). A first run reports no transitions and sayscomparedAgainst: null: diffing against an absent state file would present every existing attestation as freshly earned. A vendor seen for the first time counts as detected, never as newly attested — nobody did that work this period. -
An opt-in
--jobs Nonrunandinventory-scan. A 53-repo fleet scan takes 15–25 minutes, and the per-repo AST sweep and the fleetgit pullare both waiting on a subprocess or a socket.--jobs Nruns them concurrently. It is a pure scheduling knob: serial and parallel runs produce byte-identicaldrift.json,audit.jsonanddrift.md, asserted by a test rather than argued, so there is no "parallel mode" whose output anyone has to reason about separately. The default is1and takes a literal serial path — a plain loop, no executor constructed — with no environment variable and no auto-detection, so CI cannot change behaviour because someone forgot to pin a value. Two honest exceptions: the progress log interleaves by completion order under--jobs > 1(buffering it would make the log look sequential while the work was not — a worse lie than an interleaved one), and becauseast-grepis itself internally parallel, oversubscribing can push a repo past the engine's 600s timeout, so the value is capped to the machine's CPU count with a notice on stderr saying why. The byte-identity guarantee holds absent that kind of resource exhaustion, not unconditionally.
The guarantee is measured, and so is the disappointment. On a real 53-repo fleet with the
audit's network pinned off, --jobs 1 and --jobs 4 produce byte-identical inventory.json,
audit.json, drift.json and drift.md. The speedup does not survive the same scrutiny: two
paired runs on a 4-core box gave 734s vs 643s (12% faster) and then 597s vs 630s (6% slower),
while the serial baseline alone moved 23% between runs. ast-grep is already internally
parallel, so on a machine with few cores --jobs mostly competes with the engine for them.
Treat this as a knob that is safe to turn, not one that is known to pay — it has yet to be
demonstrated on hardware with cores to spare.
-
A fleet entry may name the branch to scan. A repository's default branch is not always where its code lives: several projects on a real fleet keep a README on
masterand develop ondev, so the scan read a placeholder and reported the repo as clean. Afleetentry may now be{url: …, branch: develop}instead of a bare URL; strings stay valid, so no existing config changes. A branch that does not exist on the remote fails that repository rather than falling back to the default — somebody asked for specific code and did not get it, and scanning something else while reporting findings against it is the failure this tool exists to refuse. A branch on a group URL is refused too: one branch name is not guaranteed to mean the same thing across every repo under a namespace, and a per-repo fallback would produce a scan mixing branches with nothing in the report saying which.ref_is_defaultstops being a hardcodedtrue, so a scan ofdevelopcan no longer read as a scan ofmain. -
CVE lookups are batched instead of one request per package. A fleet audit made one
POST /v1/queryper unique package — 642 sequential requests on a real fleet, minutes of wall clock, and the shape that trips rate limits. That last part made it a correctness problem as well as a speed one: a rate-limited run emits⚠ DEGRADEDand reports fewer findings without being wrong on its face. It now sendsPOST /v1/querybatchin chunks and then fetches each unique advisory once — the same CVE recurs across repos and packages, so the advisory set is far smaller than the occurrence count, and that collapse is where the saving comes from rather than concurrency. Findings are unchanged: a test asserts the batch and per-package routes normalise to identical dicts, including an advisory that lists the same package name under two ecosystems. Pagination is followed per query, because OSV pages once a queryset passes 3,000 vulnerabilities and stopping at the first page would report real findings as absent.
Measured on a 53-repo fleet against the live API, 596 unique package keys: 180 requests instead of 596 (3 batch + 177 advisory fetches), and ~90s instead of 311s. Both routes produced the same 568 CVE findings. The request count is the number that matters most — 596 sequential requests is the shape that trips rate limits, and a rate-limited run is a quieter report, not a louder one.
Each HTTP call is retried up to three times on a transport fault. That is not belt-and-braces:
before it, the batched audit failed three runs in five with [Errno 104] Connection reset by
peer, and each failure discarded all 568 findings, because the advisory phase makes ~177
requests and any one of them ending the run ends the audit. With retries it succeeded five times
out of five. A malformed response is not retried — that is a protocol violation no repetition
fixes — and a genuinely unreachable OSV still degrades the source loudly once the attempts are
spent.
- A scan now ends where the answer ends.
drift-scan chat-summaryrenders the closing block a CLI run finishes with — headline, movement since the last scan, the most urgent retirement, what to do first, what the scan could not see, and where every report lives. Roughly fifteen lines, emitted last and verbatim. This changes the plugin's primary output for every user, and deliberately: the command file used to specify the report followed by five more blocks — a link list, honesty surfaces, a scheduling pitch with crontab mechanics, and a cleanup offer, three of which asked the reader to decide something. Someone who came for a result was handed a setup interview, and the output never visibly ended, so there was no way to tell whether more was coming. Scheduling and cleanup still exist and are still discoverable — one fixed footer line says so, and their mechanics moved to the management-modes section where somebody asking will find them. The block is a pure function ofdrift.json, like every other surface here, so it reads the same on every run and under every model. The AI plane's leads are untouched: they print in full, above, and the block accounts for the pass in one line without restating it. - A summary can be emailed when a scan completes (
drift-scan email-summary,notify.email). Everything this tool publishes is pull — somebody has to go and look, and nobody reliably does. Recipients live indrift.ymlbecause they are not secrets the way the SMTP password is; the credential stays an env-var name, asnotify.gchatestablished. Sent from its own CI job, so no SMTP credential enters the scan path. It ismultipart/alternative— HTML alone breaks in the places an operations mail actually gets read — and TLS is not optional: asmtp://URL whose STARTTLS fails raises rather than falling back to cleartext, because the body names client repositories. Unlike the chat notifier, a failed delivery exits non-zero: the mail goes on every completed scan, so its absence means no scan, and a silent failure would destroy that signal.
Fixed¶
-
A dated API version was refused as though it were a claim about the future. The leads gate scans every string field of an AI-proposed integration and refuses any date shape — correctly, because a lead may say whether something is retired, never when. But Amazon SP-API names its endpoints by date, so
version: "2020-09-04"is an identifier copied out of the source at a citedfile:line, asserting nothing. The gate was therefore unable to express a true lead about the product's flagship vendor.versionnow takes a narrow exemption: a bare ASCII token and nothing else. Prose in a version field (retires 2027-03-01) is still refused,noteis scanned exactly as before, and the exemption's alphabet is 10 characters wide — no sentence, and therefore no claim about when something retires, can ride along inside it. Anendpointexemption was attempted and deliberately withdrawn: no lexical rule separates a real dated path segment from a claim written to look like one (/2027-03-01/sunset-per-the-vendor-changelogis the same shape), and the three attempts are recorded in the code so nobody restores it as a missing feature. Corroborating a dated endpoint against what the deterministic scanner observed is the real fix, and is specced separately. -
catalogSummarynever reacheddrift.json. The absorption counts existed inaudit.jsonand stopped there, so the canonical contract — which every other surface is a verified projection of — could not state them. Both it andcatalogDeltaare now pinned indocs/schema/drift-v1.schema.json. -
A signed-off vendor would have joined the human freshness work-order, asking someone to go read the deprecation page of a library we wrote ourselves.
due_for_refreshskipped onlyCURRENTandBLOCKED; it now skips the two dispositions too, and they return on their own when the expiry lapses them. -
A run in which every repository errored printed
✓ scan+audit: 🔴 0 · 🟠 0and exited 0.coverage.reposErroredwas recorded by the scan and then reached nothing a caller sees — not the banner, not the exit code — andreposScannedcounts errored repos too, so "could not read a single repository" rendered identically to "read everything, found nothing".runnow prints a ⚠ line naming the errored repos with their reasons, and when every discovered repo errored it follows the "scanned 0 repositories" precedent: an explicitNOT a clean resultand exit 4. stderr and the exit code only — the artifacts are untouched. - Two repositories with the same directory name under different roots shared one cache entry.
discover_reposguarantees collision-free identities only within one call and each root is a separate call, so two roots each holding aweb/both yield the identityweb; sitting at the same commit, the second was served the first's cached record and reported using another repo's results. The cache is now bypassed entirely for a colliding identity — never loaded, never saved, always scanned fresh. (Making identity globally unique is the proper fix and would move every cache key and rendered repo label; it is deliberately left alone.) -
The per-repo cache was written by truncate-then-write, so a concurrent reader could see a half-written file and report a perfectly scannable repo as errored. It is now
mkstemp+os.replace, matchingabsorb_trail.forget. -
The scanner was not byte-reproducible, and had not been for some time. The same repository scanned three times in a row — no concurrency, default settings — produced three different
inventory.jsonfiles: identical in size and in the set of records, differing only in which of two entries sharing onefile:linecame first.agent/lib/endpoints.pycanonicalises for the engine's unstable match order in two places, and both keys were incomplete — the processing walk sorted on(url-first, path, line)and the residue lists on the location alone. Two rules routinely match one line, so both keys tied, and a stable sort resolves a tie by keeping the arrival order the key exists to discard. Worse than cosmetic on the walk:seen_knownis first-wins, so the engine chose which same-key record survived and what a group'sexampleshowed. Both keys are now total. This is principle 3 ("same inputs → byte-identical output"); it was found by diffing two full runs of one fleet, which nothing had done before--jobs. -
A repository the scanner could not read reported
KNOWN. A repo holding only a README — no manifest, no source in any language the ruleset covers — collected no findings and no reasons, and was published asKNOWN: "we looked, it is fine". A repo that was read and genuinely contained no API calls was honestlyUNKNOWN, so the repo we could not see scored healthier than the one we could. Every check inverdictwas guarded on having languages, and an empty repo has none. It now carriesno-readable-sourceand isUNKNOWN, andverifyrefuses a document that says otherwise. This will move counts on any fleet containing such repos — correctly, and for the first time visibly. A genuine docs or runbooks repo is now permanentlyUNKNOWN; quieting that needs an acknowledged-empty attestation, with an approver and an expiry like every other disposition here, which is deliberately not invented yet. -
A config error could print a pasted credential into the CI log.
_env_nametruncates a rejected value when it matches a known secret prefix, but its other refusal echoed the value in full. An SMTP URL matches no prefix and sits under the length cap, sosmtps://user:PASSWORD@hostwould have appeared verbatim in the output of every failedconfig-preflight. Both messages now truncate. Found while writing a test that asserted the refusal must not echo what it was rejecting. - The findings delta could not tell a first scan from a catastrophic week. It reported
newandresolvedwith nothing saying what it compared against, so a fleet's first run — where every finding is new — rendered identically to a week in which everything changed. On a real fleet that was 349new.delta.comparedAgainstnow names the previous scan, or is null when there was none, matching whatcatalogDeltahas always carried.
v1.0.0 — 2026-08-21¶
First stable release. Supersedes the 0.19/0.20 betas, which shipped without changelog
entries; this covers everything since v0.18.0-beta (2026-08-12). The -beta suffix is gone and
the version now means one thing — the plugin. The correctness claim is unchanged and remains the
only one worth making: drift-scan verify passing is the claim; nothing else is.
Added¶
- Endpoint attribution beyond the host. A vendor can now be identified by what its URLs look
like, not only by the host they point at.
pathSignaturenames a vendor + version from a distinctive path (/dw/shop/v24_5), including on bare path literals and on runtime-assembled URLs where the host is a variable.modelSignaturedoes the same for the AI category, whose retirements are published per model id rather than per endpoint — corroborated against the repo's SDK dependency so a model name in a comment cannot attribute on its own. - Vendors with no host at all. Salesforce Commerce Cloud (OCAPI) and Magento are served from each merchant's own domain, so no domain list can ever name them. They are catalogued by path, and the call-site's observed host becomes the record's label.
BLOCKED— a fourth catalog verdict. For vendors that publish retirements only behind a partner or seller login. Previously indistinguishable from "nobody got to it", which sent readers after the wrong fix and kept the freshness work-order permanently non-empty with a task that can never succeed. BLOCKED is not an attestation: it never ages into CURRENT, its call-sites keep counting as unchecked exposure, and it must carry the gate page actually hit. Its provenance nests inside theblocked:key so that a scanner predating the verdict reads the entry as UNAUDITED rather than CURRENT — data may ship ahead of the code that understands it, and an unknown verdict must fail toward under-claiming.uncatalogued-vendorandwhole-api-retired— a detected host with no catalog entry is now its own verdict rather than silence, and a vendor whose entire API is already retired is not reported as "unaudited".- The vendor-resolution queue files itself (
resolve_stream). Unnamed hosts were recomputed every scan and thrown away; the queue reached 28 hosts deep before anyone looked. It is now a self-updating work-order that closes when it empties. - The absorb trail — every
absorb --checkattempt is recorded, so the climb from residue to attribution is auditable rather than a claim about work that already happened. - Inventory extractors for Go, Maven, Gradle, NuGet, Bundler and Cargo, plus NuGet central package management and lockfile version joins.
- Documentation site (MkDocs Material, GitHub Pages): a landing page, a plain-English "how it works", a guide to reading the report with screenshots rendered from the public corpus, a rewritten FAQ, and architecture diagrams.
- Container channel — a published image, so a fleet runner needs no Python toolchain.
- Catalog coverage: Amazon MWS dated and attested, eBay Post-Order, Amazon SP-API operation paths, Anthropic and Mistral model retirements, Amazon Ads path-scoped sunsets, Login with Amazon, and dead-marketplace closures (Catch, MyDeal, MySale, TheMarket).
Fixed¶
- A vendor definition could crash an entire repo scan. A catalogued vendor with a path
signature and no domains raised
IndexError, and the run still printed "0 action-required" beside the errored repo — the exact shape of report this tool exists to prevent. - The report contradicted its own work-order: a host could be named and dated as a finding while simultaneously queued for a human to go identify it.
- A declared asset host was claimed by a parent-domain vendor rule.
fonts.googleapis.comis declared an asset CDN, but thegoogleapis.comvendor rule overrode it — so every page loading a Google Font counted as a live API integration and put the vendor on the audit backlog for a<link rel=stylesheet>. - Call-site counts silently maxed out at six, understating exposure in the one column readers use to judge it.
- Precision, measured on a real corpus: XML namespace URIs are identifiers, not endpoints (−589 sites, no real findings lost); an SDK's own service descriptors are not call-sites (−380, with SP-API attribution unchanged at 945); documentation hosts are not unresolved integrations; vendored UI libraries are skipped by filename with a token boundary.
- Path-constant attributions were counted as residue, making coverage look worse than it was.
- The client-identifier guard now runs at push, not in CI, and refuses when its deny-list is unset — a guard that silently passes is worse than no guard.
v0.18.0-beta — 2026-08-12¶
Coverage follow-ups — SDK-mediated detection, batch 2, and a freshness loop.
Added¶
- SDK-client detection — surface SDK-mediated vendors from the manifest. Closes the
sdk-only-no-callsiteblind spot for the deterministic scan: a repo that reaches an API through an SDK (twilio/sdk,@sendgrid/mail— method chains, config-injected URLs) has no scannable host literal. Newsdk_clients.yamlmaps API-client packages → vendor+host; a dependency injects a synthetic endpoint (attributionsdk-client, evidenced atcomposer.json). Proven: zenithapp-crm now surfaces Twilio + SendGrid — vendors the deterministic scan missed entirely before. - Batch 2 — 9 more vendors pre-audited. Etsy, BigCommerce, WooCommerce, Magento, Kogan, Trade Me,
Tradevine, Marketplacer, Firebase FCM tracked-current. 4 refused as honest "unverified" (Rakuten,
Amazon Ads, UPS, Twitter/X — JS-only or login-gated; no guessing). 50 vendors attested total.
(Firebase FCM's legacy HTTP/XMPP shutdown, 2024-06-20, is real and sourced but held from the sunset
catalog: its
/fcm/sendpath is version-less so the endpoint model captures noapiPathto scope on, and a host-scoped entry would over-flag the healthy v1 API — awaits version-less path capture.) - Stale-attestation loop.
catalog-checknow lists every attestation past its 90-day TTL as re-research work and prints theresearch --vendors "…"re-run command — so the pre-audit doesn't silently rot.
v0.17.0-beta — 2026-08-11¶
Pre-audited mainstream vendors — demos stop hitting "unaudited" blanks.
Added¶
drift-scan research --vendors— batch/catalog research. Lists everyvendors.yamlentry with no attestation (the demo-blank tail), no repo needed;--apply … --attest … --now …gates a completed AI pass and writescurrentverdicts asai-researchattestations (UNAUDITED → tracked-current), reportingretiringverdicts for absorb. Two trust guards, each tested against its bug: mega-vendors (Google APIs, Amazon AWS, …) refuse a blanketcurrent(they retire services constantly — must be scoped per product); acurrentattestation must cite a real deprecation/changelog/versioning page with an excerpt — a login/redirect/product-only source is rejected (the live Seller Snap 302 bug).- 29 mainstream vendors pre-audited. A batch pass reconciled 29 vendors against their own
deprecation pages and recorded attestations: payments (Stripe, PayPal, Braintree, Square, Adyen,
Razorpay, Authorize.Net, Klarna, Checkout.com), comms (SendGrid, Mailgun, Klaviyo, +sunsets for
Twilio/Mailchimp/Vonage/Slack), AI/dev (OpenAI, Anthropic, GitHub, Google Maps/OAuth2, Meta Graph,
LinkedIn), shipping/tax (ShipStation, EasyPost, Shippo, Avalara, TaxJar, +FedEx sunset). A scan
that used to say "7 unaudited" now says "tracked-current, last checked
." - 3 dated sunsets, scoped to their retiring surface only. Of 8 retirements the pass found, three
join cleanly on the endpoint model and were added — OpenAI Assistants API (
/v1/assistants, 2026-08-26), Mailchimp Export API (/export/1.0, 2023-06-01), FedEx SOAP (ws.fedex.com, 2026-06-01) — each proven to flag ONLY the retiring path/host (a fixture using/v1/assistantsflags;/v1/chat/completionsdoes not). The other five (Twilio region-domains, Vonage, Slack classic-apps, Anthropic models, Google Maps KmlLayer) are recorded in the attestations but held from the sunset catalog until operation/model-marker detection lands — adding them broad would flag healthy usage.
v0.16.0-beta — 2026-08-11¶
The last mile — turn-key CI deployment.
Added¶
/drift-detector onboard <repo>— one command to a scheduled deployment. Detects the platform (GitHub Actions or GitLab CI), scaffolds a scheduled workflow that installs the plugin and runsclaude -p "/drift-detector …", wires the client's ownANTHROPIC_API_KEYas a CI secret, opens a PR/MR, and self-verifies the run — so onboarding proves it works, not just leaves YAML. Two hard guardrails: the API key never passes through the session or the repo (the user sets it directly viagh secret set/glab variable set), and changes land on a branch + PR, never the default branch. Single-repo by default;--fleetopts into multi-repo scanning with a PAT. Templates ship intemplates/ci/{github-actions,gitlab-ci}.yml.
v0.15.1-beta — 2026-08-11¶
The plugin runs its own engine, headless-ready — and the PyPI channel is gone.
Fixed¶
- The plugin now always runs the engine it ships.
/drift-detectorpreviously preferred auvx --from drift-detector-scanPyPI package, which had drifted onto a separate version line and silently ran a stale engine — producing dashboards that FAILED this plugin's ownverify(e.g.sqs.*.amazonaws.com → hostClass None). The runner now resolves the bundledbin/drift-scanfirst and only that, guaranteeing engine == orchestration == verify. A regression test intests/test_runner.pyasserts theuvx --fromrunner can never come back. - Headless /
-pruns complete unattended. In print mode with sources already given, the command skips the interactive plan-approval + report-sharing gates and scans local-only, soclaude -p "/drift-detector <repos>"runs to a verified report in CI.
Removed¶
- The PyPI distribution channel.
pyproject.toml, thepublishworkflow, anddocs/PUBLISHING.mdare gone — the plugin is the product, and it ships its own self-provisioning engine (bin/drift-scan: a venv fromrequirements-plugin.txt+ the pinned ast-grep binary). Nouvx/pipxinstall path, one fewer CI workflow, no version-skew surface. - The GHCR container channel.
Dockerfile,.dockerignore,.github/workflows/container.yml,docs/CONTAINER.md,tests/test_container.py— gone too. It was a separate no-AI deterministic CI runner; with CI going through the plugin (claude -p), it was unused surface. Recoverable from git if a no-Claude CI path is ever needed. (Dockerfile scanning of target repos —runtime_pins— is unaffected; that reads clients' Dockerfiles and stays.)
Added¶
- Dailymotion + Esri ArcGIS sunsets folded from the local research overlay into the committed catalog, so a fresh clone carries them (each sourced + dated, entered via the absorb gate).
v0.15.0-beta — 2026-08-10¶
The complete integration inventory — and the tool teaches itself.
Added¶
- Complete "Detected" inventory as the headline. Every outbound endpoint the engine reads is
now one flat, exportable list (Host · Kind · recognized-as · call-sites · coverage), with a
verifyinvariant that the shown count equals the real endpoint count. Classification collapses to four human buckets (API integration / third-party service / asset-library / your-infra) as a filter, not tiles that fragment the list. Deficit language ("unclassified/unaudited") → inventory language. Includes a client-side Export endpoints (CSV). - Coverage lifecycle. Each endpoint carries a
coveragestate —tracked · queued · needs-human · blocked · na— thatverifyproves partitions the total. "Untracked" is now a resolving queue, not a dead-end. /drift-research— the self-teaching loop. For eachqueued(detected-but-uncatalogued) API service, an AI reads the vendor's own deprecation docs in the wild and returns a sourced verdict; the deterministicresearchcommand gates it (a retirement's date must appear verbatim in its fetched excerpt — no invented or inferred dates) and records it. The AI Frontier plane now shows what the tool taught itself (vendors researched, sunsets found, sourced).- Own-infra detection. Account-cloud endpoints (Cognito/API-Gateway/serverless), dynamic-DNS hosts, and multi-subdomain own domains are recognized as your infrastructure, not vendors.
- Attestation provenance + TTL. Attestations record
by: human | ai-research; an AI "current" is surfaced distinctly and expires under the existing 90-day re-check TTL.
v0.13.0-beta — 2026-07-21¶
See what's already broken — and a charts view.
Added¶
- "Past-due" tile + report row. A vendor API that is already retired (past its
removal date) is a different, more urgent thing than a CVE fix or an upcoming deadline —
an integration broken now. It gets its own count (
counts.pastDue), a Past-due tile in the dashboard's Integrations group, and an "— of which already retired (past-due)" row in the Markdown summary.verifyguards the new number against drift on every surface. - A "Most urgent" callout at the top of
drift.md, naming the single most pressing surface (the most-overdue retired sunset, else the soonest deadline) so the reader has one thing to do first. chart.html— an online charts view. The same report data drawn as a risk doughnut, a per-vendor retired-vs-upcoming bar, and a most-overdue-first retirement schedule. It loads Chart.js from a CDN, so it needs internet; if the CDN is unreachable it says so and points back at the dashboard.dashboard.htmlstays self-contained and offline — the charts view is a separate, additional file. It embeds the same verified payload, soverifyproves the charts draw fromdrift.jsonand nothing else.
v0.12.1-beta — 2026-07-21¶
Fixed¶
- Corrected the plugin's authorship — author and marketplace owner are now Laxit Patel (the creator), and the LICENSE copyright matches. No functional change.
v0.12.0-beta — 2026-07-21¶
Scan a whole client fleet from one URL — and a build you can reproduce.
Added¶
- Scan a whole GitLab group or user namespace. Point at
https://git.example.com/acmeand the tool enumerates every repo the token can access under it — group or user namespace — clones each, and scans the fleet. You cannot miss a repo you didn't list. It enumerates viamembership=true(so group-inherited, user-owned, and direct-member repos all appear), a URL that is itself a project clones directly, and a mid-enumeration failure aborts rather than silently scanning a subset. The plan/approve step previews the whole fleet before any scan runs.
Changed / reproducibility¶
- The ast-grep engine is now pinned (0.44.1; override via
$DRIFT_AST_GREP_VERSION). Previously it fetched "latest", so two machines could get different engines and silently different output — at odds with the tool's deterministic guarantee. First run on an existing install re-fetches the pinned engine. - Rule metadata now travels with each match (
--include-metadata), so a scan no longer re-reads the rule file — one fewer failure point, same result. verifygained a number-format check — every number indrift.jsonmust serialize identically across environments (no exponent, ≤1 decimal), guarding byte-identical output.
Packaging & metadata¶
- Fixed the marketplace listing — it advertised the wrong engine ("Opengrep") and a
stale version. Now correct and synced to
plugin.json. - Added
LICENSE(MIT), andhomepage/repository/license/displayNameto the manifest. - Removed leftover build cruft and internal planning docs from the package.
- The plugin now carries its collection identity: Ashen Oracle — Know before it breaks.
v0.11.2-beta — 2026-07-21¶
Fixed¶
- Advice now reads correctly against today's date. The report used to say "plan
migration before 2025-01-21" for a date already long past. A retirement whose date has
gone now reads "migrate off this API NOW — already retired 2025-01-21"; only a future
retirement shows as a "before
" deadline. The finding's status was already date-aware (past = red/action-required, future = amber/review); this brings the wording in line.
v0.11.1-beta — 2026-07-21¶
Fixed¶
- A scan across repos that vendor the same SDK no longer fails verification. When the
same finding (a shared vendor SDK, a common runtime) appeared in two repos with an
identical repo-relative call-site, the Markdown report rendered byte-identical rows and
drift-scan verifyrejected it. The findings tables now lead with a Repo column, so each repo's exposure is its own row — the disambiguator, and the thing you most want to know (which of my repos does this hit). Presentation only; finding totals unchanged, and the dashboard already showed the repo.
v0.11.0-beta — 2026-07-21¶
Fix: the plugin could silently run a stale cached build.
When CLAUDE_PLUGIN_ROOT was unset (ad-hoc shells, and especially scheduled cron
runs), the runner locator fell back to find … | head -1 — which picks a build by
directory order, not version, and could grab an OLD cached copy. Symptom: a new
subcommand failing with an argparse error, because an older scanner was executing.
Fixed¶
- Version-aware runner location. The command files now consult
installed_plugins.json(authoritative) first, then fall back to the newest cached build by semver (sort -V, neverhead -1— lexically0.10.0-betasorts before0.4.0-beta). Applied to both/drift-detectorand/drift-deepen. - Scheduled runs follow upgrades. The cron wrapper used to pin the runner path at
install time, so a job kept executing the version that was current when it was
scheduled — even after upgrading. It now resolves the installed runner at run time.
If you have an existing schedule, re-run
/drift-detector schedule <folder>once to regenerate the wrapper with the fix. - Self-check. The runner warns (never fatal) when a cached build runs while a newer one is installed — a stale build no longer executes completely silently.
Note¶
Superseded cache directories are the plugin host's to garbage-collect; the version-aware locator makes any leftover stale build inert rather than a decoy.
v0.10.0-beta — 2026-07-21¶
Two more vendors, a picture of your exposure, and a guided flow that plans before it scans.
Added¶
- Shopify — the first vendor whose retirement dates are computed, not curated.
Shopify versions by calendar quarter (
2024-01) and publishes a rule (a version is accessible for 12 months + 15 days from release), so every version dates itself with no per-version catalog entry, and the coverage can't go stale. The rule is verified against all seven rows of Shopify's own published support table. Carries Shopify's twist: a retired version isn't a 4xx — Shopify silently serves the oldest version, so a stale pin is invisible drift. - Walmart Marketplace — 6 sourced sunsets from the vendor's deprecation guide (two
already retired). Adding it fixed a real detection gap: Walmart front-loads the version
(
/v3/insights/refunds), which used to collapse every Walmart call into one/v3record. Sub-APIs are now scoped apart — the same granularity Amazon already had. - Exposure graph — a Mermaid flowchart in
drift.md: each repo → the retiring API surfaces it calls, red for already-removed, amber for deadline-ahead. Renders natively in a Claude artifact, VS Code, and GitHub. A complement to the findings table (every node is also a row), with a structural check so a broken graph failsverifyrather than rendering a silent error box. drift-scan plan— resolves and classifies every source (git repo / plain folder / cloned / error) without scanning, so a run can be previewed and approved first.
Changed¶
/drift-detectoris now a guided flow: an intake menu when no source is given, a plan you approve before any scanning, and a delivery that renders the report inline and links every representation (Markdown, Dashboard, Data, and the Artifact if you opt in). No more per-run "want the dashboard?" question.
Vendor coverage¶
Four vendors fully audited, each from a different source shape: Amazon SP-API (page + OpenAPI specs), eBay (structured RSS feed), Shopify (computed rule), Walmart (deprecation guide).
v0.9.0-beta — 2026-07-21¶
One canonical report, three views that cannot disagree — and a report you can read in the chat, not just open in a browser.
Added¶
drift.json— the canonical, machine-readable report, now with a published contract atdocs/schema/drift-v1.schema.jsonand aschemaVersionfield. This is the spec; everything else is a view of it. (Renamed from the internaldashboard.json.)drift.md— the report as Markdown: the alarm headline, a summary table, per-family findings with dates and call-sites, and both coverage verdicts. It renders in any Markdown surface — a terminal, VS Code, GitHub, or inline in a Claude chat — so the report no longer requires opening an HTML file. Because its source is plain text, it is also the view an agent can actually read and check, which the HTML never was.drift-scan verifynow certifies all three agree. A green line meansdrift.md,dashboard.htmlanddrift.jsonare the same data — the claim anyone (or any agent) is allowed to make about the report. It re-parsesdrift.md's tables (splitting on unescaped pipes) and fails on a column that an unescaped|would truncate on GitHub, a summary number that disagrees with the data, or two findings rows that render identically.- Findings now show call-sites (located files) rather than a match count, and each carries its own retirement/EOL date column.
Changed¶
dashboard.htmlis now one viewer among several rather than the report itself. It is unchanged in content and still self-contained;drift.mdis the primary view./drift-detectorverifies before reporting, reports fromdrift.md(not by eyeballing the HTML), surfaces per-vendor catalog verdicts, and can publish the report in-chat.
Upgrading¶
- The state directory now also contains
drift.jsonanddrift.md. Anything that readdashboard.jsonshould readdrift.json(identical content, canonical name). - No cache-schema change.
v0.8.0-beta — 2026-07-21¶
Point it at anything — a checkout, a folder, or a URL — and a scan of nothing can no longer look like a clean bill.
Fixed (the one that mattered)¶
- Scanning zero repositories is now an error, not a green checkmark. Pointing the
tool at a folder with no
.git— a client's zipped source, a wrong path, a URL — used to report🔴 0 action-requiredat exit 0. It scanned nothing and declared victory. Now it exits 4 and says why: "has source files but no .git — git init it, or clone the repo", "looks like a URL — clone it first", "does not exist". This is the failure a scan of real Amazon SP-API code hit: the folder had no.git, so nothing was read, and the report said clean.
Added¶
- Scan a checkout, a plain folder, or a git/GitLab URL — one or many, mixed.
- a plain folder (no
.git) is now scanned as one project; the report notes it has no history, so "changed since last scan" and clickablefile:lineare unavailable for it — clone the repo to get both. - a git/GitLab URL is cloned into
<state>/sources/and scanned. Private-repo auth reuses your machine's own git setup — ifgit clone <url>works in your terminal, it works here. AGITLAB_TOKENin the environment is honoured via a transient credential that is never written to.git/configor the tool's state. - a bad root among good ones is reported and skipped, not silently dropped.
Upgrading¶
- No cache-schema change; existing scans are unaffected.
- A run that resolves to zero projects now exits 4 (couldn't verify) instead of 0. If a CI job was passing by scanning nothing, it will now correctly fail — that was a false pass.
v0.7.0-beta — 2026-07-20¶
The tool now reports what it has not been taught, and can see seven more languages.
Added¶
- Catalog coverage per vendor —
CURRENT/STALE/UNAUDITED. Until now a vendor with hundreds of call-sites and an empty catalog rendered exactly like a vendor that was genuinely clean; that is how eight already-past Amazon retirements stayed invisible. The unit is an attestation — "somebody opened this vendor's deprecation page on this date" — deliberately not an entry count, which is gameable by one junk entry and unknowable from the inside. New "Vendors unaudited" tile and panel. - Consequence, by design: eBay reads UNAUDITED despite having 12 catalog entries, because nobody has reconciled eBay's own page. Amazon SP-API reads CURRENT (checked 2026-07-20). This grades our coverage honestly rather than flatteringly.
- Egress detection for all 8 languages. JavaScript, TypeScript, Python, Go, Java, C#
and Ruby now have HTTP sink rules; previously only PHP did, so every other language
reported
UNKNOWN / no-egress-signaland could never be scanned with confidence. Every pattern was verified against a real fixture in that language before shipping, and those fixtures are committed as tests that run the real ruleset through the real engine — because a sink rule that matches nothing is worse than no rule: it reports coverage the scanner does not have.
Upgrading¶
- Caches invalidate once (schema 5 → 6) because the ruleset changed; the first scan after upgrading re-reads every repo.
- Expect a new UNAUDITED count. It is not new risk — it is risk that was always there and previously rendered as clean.
- Repos in the seven newly-covered languages may move from
UNKNOWNtoKNOWN.
Known gaps (unchanged, stated plainly)¶
- Five eBay operations visible on the vendor's deprecation page are still missing
(
getProductCompatibilities,updatePaymentInfo, Return Management, Business Policies, Media API).developer.ebay.comcould not be fetched, and the reachable secondary sources disagreed on dates, so no entry was written — an unsourced date is the one thing this catalog refuses. eBay's UNAUDITED status reflects exactly this. - Sink→endpoint linking still needs dataflow and remains out of scope; unresolved sinks do not affect a verdict when calls are otherwise attributed.
v0.6.0-beta — 2026-07-20¶
Amazon SP-API is now audited, and the report is now checkable by machine. v0.5.0-beta reported zero sunsets for a repo with 272 Amazon call-sites. That read as "clean" and meant "we never loaded Amazon's list". Both halves of this release come from that: the data that was missing, and the reason nobody noticed.
Added¶
- 8 Amazon SP-API retirements, fetched from the vendor's own deprecation schedule.
On a real SP-API client, six of the eight have already passed — including
/fba/inbound/v0(removed 2025-01-21) with 34 call-sites, plus/reports/2020-09-04and/feeds/2020-09-04(both removed 2024-06-27)./orders/v0(2027-03-27) and/finances/v0(2027-08-27) carry live deadlines. - The API-family axis. Amazon retires per (family, version), not per version:
four different APIs share the string
v0with four different fates. Endpoints now carryapiPath(/products/fees/v0), catalog entries can scope onpath:, and the join precedence is operation > path > domain > version. Without this aversion: v0entry would have dated 78 call-sites identically and invented most of them. drift-scan verify --state <dir>— mechanical invariants over the report: every tile equals the rows its filter yields, the sunset count is re-derived independently from findings, no two rows render an identical label, every action field is projected or explicitly declared dropped, and the page's embedded data matchesdashboard.json. Exit 0 clean / 3 violations / 4 nothing to verify.dashboard.json— the payload the page embeds, written to disk. One object, two sinks, so what a test asserts on is what a reader sees.
Fixed¶
- Sunset actions collapsed by vendor. Twelve dead eBay operations with eight
distinct dates rendered as ONE row and a tile reading
Sunsets 1; the row also kept only the highest-ranked recommendation, silently discarding the rest. Sunsets now key on the thing being retired, and rows are labelled with it (eBay GetCategoryFeatures). - A silently dropped catalog.
load_sunsetsfiltered onversion|domain|operation, so everypath-scoped entry was read, discarded without a word, and the audit still reported clean. An unscopeable entry now raises instead of vanishing. - The absorb gate rejected legitimate undated deprecations, which the catalog format
explicitly permits. It now accepts them with an explicit
status: deprecated-no-date, so "the vendor set no date" and "I could not find the date" stay distinguishable. - The dashboard header read
1 reposon a two-repo scan; it now reads1 of 2 repos affected.
Upgrading¶
- Caches invalidate once (schema 4 → 5, endpoints gained
apiPath). The first scan after upgrading re-reads every repo. Oldrepos_v4/directories are inert. - A hand-edited
vendor_sunsets.yamlwith a scopeless entry now errors instead of being skipped. That is deliberate — give the entry aversion,domain,operationorpath. - Expect more findings, not fewer, on repos using Amazon SP-API.
v0.5.0-beta — 2026-07-20¶
The release that answers the demo. The PM asked why the scan "skipped"
getCategoryFeatures. The answer was structural: our detection unit was the host,
and eBay retires operations — one host, one path, ~19 calls on independent
lifecycles. There was no way to express "GetCategories is dead, GetItem is alive."
This release adds that axis, and then makes the tool say plainly where it still
cannot see.
⚠️ Breaking — read before upgrading¶
- Reports are now ONE file:
dashboard.html.AUDIT.md,INVENTORY.md,DRIFT.md,bom.json(CycloneDX) andfindings.sarifare no longer written. The drift delta and the ranked fix queue those carried now live in the dashboard. If you had a bookmark or a pipeline reading one of those files, it will find nothing. - Removed surfaces: the MCP server (
bin/drift-mcp), the GitLab sync connector, and the GitHub Action (action.yml). CI still works —bin/drift-scan run --fail-on-deprecatedand its exit codes (0 ok / 2 error / 3 gate-tripped / 4 couldn't-verify) are unchanged and are the interface for any runner. - Engine swapped: semgrep/Opengrep → ast-grep, a static binary the runner fetches automatically on first scan. No action needed; a leftover semgrep in an old venv is ignored, not used. Scans get substantially faster.
- Caches invalidate on upgrade (schema 3 → 4), so the first scan after upgrading
re-reads every repo. Old
repos_v3/directories are inert and can be deleted.
Added¶
- The operation axis. Endpoints carry
operation, sunsets can be scoped to one, and the audit join is operation > domain > version. This is what makesGetCategoryFeatures(decommissioned 2026-06-04) reportable at its exactfile:linewhileGetItemon the same host stays quiet. - Coverage verdicts — KNOWN / UNKNOWN with reasons. Derived from what the ruleset
can actually see per language. A Go-only repo can no longer report a confident
grade off zero signal; it reports
UNKNOWN (no-egress-signal)and routes to a manual pass. This will look like a regression and is not — it is the tool admitting a blindness it previously hid. - Residue — versioned paths and egress calls we matched but could not attribute,
listed with
file:line. The scanner's own conscience; it is what a scout pass reads. observedvsinferredattribution. When only one vendor is present, a bare path is attributed to it — a guess about the repo, not evidence from the line. That is now labelled. Worth knowing:amazonspapiis 2 observed / 18 inferred./drift-deepen <folder>— the scout. Investigates only what the scan admits it cannot read, and must pass a deterministic gate (drift-scan absorb) that re-scans and rejects any proposal that does not hold up. Dates without a fetched source are refused outright.drift-scan recommend— per-repo scan profile (auto / hybrid / manual) with a one-line why.- 10 dated, sourced vendor sunsets — 6 eBay Trading operations plus the LMS retirements, each deep-linked to the release note that announced it.
Fixed¶
- Seven bugs from a deliberate adversarial self-audit — malformed engine output reading as a clean scan; heredoc URLs lost; orphan operation markers dropped; an unreadable repo reporting KNOWN; the absorb gate passing an over-attributing proposal; attestations bleeding between same-named repos; a grade that could read HIGH beside a verdict of UNKNOWN. Every one was reproduced before it was fixed.
v0.4.0-beta — 2026-07-17¶
A measurement instrument for the scanner: run it against real code and see what it catches.
Added¶
- Evaluation / regression harness (
bin/drift-eval, contributor tool — see docs/EVAL.md). Clones a pinned corpus of real public repos grouped by the integration they use (eval/corpus.yaml), scans them, and scores the scanner: recall is a hard gate (a repo insandbox/ebay/must detect eBay), plus informational noise / version / sunset metrics. Every miss is tagged by a failure-mode enum so the scorecard doubles as an improvement backlog. Deterministic, zero-LLM; clones and run artifacts live under~/.drift/and~/Projects/sandbox/, never committed. - Corpus: eBay (5 repos), Amazon SP-API (5), Walmart (4) — all real, SHA-pinned. First scores:
eBay 5/5 recall + the
svcs.ebay.comFinding-API sunset fired on a real legacy repo; SP-API 5/5; Walmart 4/4. ~/.drift/home for eval + central/demo run artifacts (honors$DRIFT_HOME). The plugin's in-place<folder>/.drift-detector/behavior is unchanged.
Fixed / changed¶
- Honest version-rate metric. Version-extraction rate is now measured only over endpoints whose URL actually carries a version, with a separate "no URL version" count — so the scanner isn't scored down for APIs that have no URL version (a vendor's design choice, not a scanner failure).
Notes¶
- The harness quantified a real boundary: a scanner miss where the API version lives only in SDK code (a class constant assembled at runtime) is deterministically unreachable — it marks where a future cognition layer would earn its place, rather than something to chase with AST rules.
v0.3.0-beta — 2026-07-16¶
The report you actually act on, plus a visual surface and sharper detection.
Added¶
- Ranked fix actions. Findings now roll up into
(repo, package)actions — 30 CVEs against one package are one job (upgradetorchto2.10.0), not 30 rows.AUDIT.mdopens with "Do this first" (ranked by severity, then blast radius, each with the exact upgrade command), then the full fix queue, then per-repo. - Interactive dashboard. Every scan writes a self-contained
dashboard.html— inline CSS + JS, no server, no CDN, opens fromfile://. Clickable tiles (Critical · Fixes · EOL · Sunsets · APIs used · Unknown hosts) over a drill-down fix queue; dark/light theme. Also available on demand viaaudit --out-html <path>. - Domain-scoped vendor sunsets. Catalog entries can target a specific host, so a dead
legacy API is flagged without false-flagging a live one that shares its version string.
Ships the real eBay Finding API (
svcs.ebay.com) and Shopping API (open.api.ebay.com) retirements (decommissioned 2025-02-05 → migrate to Browse API). - Read-only GitLab connector (
gitlab-sync). Clone/pull your GitLab fleet with a read-only PAT (read_api+read_repository) into a folder, then scan it — so private and in-house wrapper repos get covered. The token is env-only and stripped from every repo's.git/config. (No GitLab MCP required.) - Coverage honesty +
doctor. The scan now reports what it couldn't see — private/ unresolvable package sources, unknown external hosts, floor-only vs lockfile-exact versions — anddrift-detector doctor <folder>runs a scan-readiness preflight. - Discover-then-classify detection. Inverted the old allow-list: one broad URL rule catches every outbound endpoint, then classifies against a ~40-vendor catalog (now including Amazon AWS). Unknown external hosts are surfaced instead of silently dropped.
Fixed¶
- Report ranking bug. "Most urgent" took the first 15 findings unsorted, burying every CRITICAL (including remote-code-execution advisories) under "…and N more". Now genuinely ranked.
- Git-SHA fix versions. OSV returns some
fixedvalues as commit hashes; the version sort ranked those above real versions and recommended a git SHA. Now filtered to real version strings. - Dashboard XSS hardening. Scan-derived strings are escaped on both surfaces
(HTML text + the embedded JSON blob), attribute contexts get quote-safe escaping, and
source links are restricted to
http(s)schemes. - Substring host mis-attribution (
ups.commatchingstartups.com) — matching is now registrable-domain / boundary-anchored. - Stale scan cache silently omitted new fields; the per-repo cache is now schema-versioned.
Notes¶
- The dashboard shows the latest run; week-over-week movement comes from the finding delta, not a multi-run archive (a future layer).
- The Google Chat webhook and any GitLab token are per-install configuration held by each user, never committed. Teammates who install the plugin get their own notifications and point at their own repos.
v0.2.0-beta — 2026-07-15¶
- Lockfile-exact versions + finding lifecycle (fingerprints,
first_seen, baseline mute, delta-first digest). - Curated vendor-API-sunset catalog joined against the endpoint inventory.
- Read-only MCP facade (
bin/drift-mcp) for generation-time prevention from any assistant. - Deterministic CI:
run --fail-on-deprecated+ composite GitHub Action + SARIF upload.
v0.1.0-beta — 2026-07-14¶
- Initial public beta: code-level integration inventory (Opengrep), drift vs last scan, OSV + endoflife.date audit, Google Chat delivery, self-scheduling cron, Claude Code plugin.