All notable changes to Traust are documented here.
- Pins traust-engine 0.28.0 (rule pack v1.9).
- The LIKE-wildcard rule catches a bound f-string parameter such as
(f"{owner}/{folder}/%",). - SSRF taint follows
request.get_json()and friends, and stops at a database fetch. - The new
traust-python-ssrf-stored-urlflags an outbound call to a URL read from a database row or model field. - A default
traust adapters opengreprun now finds both planted defects theteam-notesrecall fixture's audit run missed.
- The LIKE-wildcard rule catches a bound f-string parameter such as
- The config-matrix redacts credentials in every tier, not just Python
settings. A secure-code-audit run on the
team-notesrecall fixture found the pre-scan copying a hardcodedTOKEN_SIGNING_KEYverbatim into its artifact.- Redaction moved into the shared triple builder, so Helm, Kustomize, Compose, Kubernetes env and env-chain defaults are covered too. The env-chain tier's sink expression, which quotes the source line, is redacted as well.
- Names that count as credentials:
- any secret-class name, now also including
signing_key,encryption_key,hmac_keyandsalt - any name whose last segment ends in key, token, salt, secret or
password at a word boundary (
SIGNING_KEY,apiToken,session-secret), but notpublic_key,primary_key,sort_keyand similar - templated values (
${X},{{ … }}) are not literals and stay as they are
- any secret-class name, now also including
- A redacted default is reported as
<hardcoded literal, N chars>, markedliteral_redacted, and judged as a weak default. - Verification toggles with words between the parts now count, for
example
VERIFY_WEBHOOK_SIGNATURES. - On five real estate repos, no triple was lost and none was reclassified. 15 credential defaults were newly redacted, from env fallbacks, Compose and Kubernetes env. None of the redacted values appears anywhere in the new output.
/recall-benchmarksupports seeded fixtures.build_canaries.pynow accepts aseed.yamlanswer key beside the existingcanary.yaml: a synthetic app with one expected finding per planted defect and noinjectionblock, registered as abt-seeded-<name>target. Seeded fixtures live inprogress-tracker/configs/benchmark-seeded/, apart from the canaries, so adding one leaves the canaries' entries untouched.- The builder refuses a source with both answer keys, or with an answer
key anywhere inside
tree/(it used to check onlytree/canary.yaml). - Seeded fixtures are always development split: their author has read the answer key.
- The manifest is now written with
allow_unicode, so a rebuild no longer re-escapes every non-ASCII character in existing entries. - Materializing skips
__pycache__/and*.pyc, so a target's SHA depends only on committed content, not on who last ran Python over the source. - The skill documents seeded fixtures: one defect per file (matching is by
path and CWE), and nothing in
tree/may hint at the answers.
- The builder refuses a source with both answer keys, or with an answer
key anywhere inside
- The config-matrix pre-scan expands Python settings (new
python-settingstier inexpand_config_matrix.py).- What it reads: literals in settings modules (
settings*.py,config*.py,conf.py,configuration*.py) and in settings classes (names endingSettings,ConfigorConfiguration, or extending pydantic'sBaseSettings). Files are parsed withast, never imported or run. Test files,conftest.pyand test directories are skipped. - Sinks: each triple's sink lists the setting's readers
(
settings.X,config.X,app.config["X"],config.get("X"),getattr(settings, "X")), so the judge can rate a flag by its worst reader. - Classification: by the setting's name, so module-path values such
as
rbac.urlsno longer read as auth. A value marks only a DSN or a bind-all address (0.0.0.0). Test toggles (TESTING), CSRF and signature-verification switches are now judged. A debug toggle is weak when it's on. - Hardcoded credentials: a secret-shaped name, or a URL with an
embedded password, is a judged
weak_default. It is reported as<hardcoded literal, N chars>or with the password masked, markedliteral_redacted, and never copied into the artifact. - Unparsed modules: a settings module that doesn't parse is a
coverage_gap. - The new class words (
testing,csrf,skip_auth,verify_signature) also apply to the other tiers. On five real Python repos, the existing tiers' triples were unchanged.
- What it reads: literals in settings modules (
/secure-code-audit's config-matrix section documents the tier. Anunresolvedsink means no reader in this tree, not an unused setting, because frameworks read most of them.
/threat-modelbootstrap maps the review surfaces.- The Surface-mapper's grep table gains five rows: credential verification and minting, ownership resolution, security config flags, outbound fetch and shared content stores. Route and guard coverage, the sixth surface, stays under Network.
- Entry points those rows produce are tagged with
review_surfaces. - Stage 4's gap-fill walks each tagged surface's check from
review-surfaces.md, alongside STRIDE and the infra/IAM walk. Each plausible answer becomes a section 4 row tagged with the surfaces it exercises; a check that finds nothing goes in section 5 with the reason. - Each new row names the tag it produces.
scope-enforcementgoes on Network entry points behind a role, scope or permission check. Outbound fetch means a server-side request whose destination someone other than the operator can influence. A threat's tags are a subset of the union of its entry points' tags. The Surface-mapper Returns shape and the checkpoint templates carryreview_surfaces.
/secure-code-auditreads the threat model's review-surface tags from its JSON (<repo>-threat-model.json, which only the JSON carries). Each surface the model tags must endfindingornegative, ornot_assessedwith its gap, nevernot_applicable.- The
/vuln-scanreview brief checks the review surfaces.- "WHAT TO LOOK FOR" gains a REVIEW SURFACES group, one line per surface. A shipped default that switches a control off is reportable even though flags are operator-set; the brief's exclusion for operator-controlled flags covers flags as an attack vector, not unsafe defaults.
- The depth heuristics add (d): apply each touched surface's check.
- The brief template now carries the depth heuristics (a)–(d) itself.
- Each reported finding names its
<review_surface>: the surface where the defect sits, whatever its CWE. - Each review subagent returns a
<surface>block per surface it examined, using only the six ids. - Step 4 builds
review_surface_coveragefrom the subagents' output:findinglists every finding naming the surface.negativecombines what each subagent examined, and only when no finding names the surface.not_applicablerequires every subagent that looked to agree.- A surface nobody examined is
not_assessedon a full scan andout_of_scopeon a diff scan, nevernegative.
- In
--diffmode, a cluster that touches a verifier, a flag or its readers, an outbound call, an owner lookup, a route guard or a content-assembly path gets the matching surface check.
/vuln-scan's diff-packet builder no longer quotes a deployment's pilot cost figure in its docstring.
- Per-model cache rates and per-request long-prompt pricing. Pins
traust-contracts 0.59.0, traust-ledger and traust-engine 0.27.0.
- Cache rates:
config/model-registry.yamlsetscache_read_multiplier0.05 for Opus 5.5 and Sonnet 5.5, and 0.025 for Mythos 5.1 and Fable 5.1, per the published rate card. Every other model keeps the standard 0.1. - Haiku 5.5 long prompts:
claude-haiku-5-5gets along_prompttier. A request whose prompt (input, cache reads and cache writes) is over 100k tokens is priced at $0.50/$2.50. - Financial report:
build_financial_report's transcript collector splits long-prompt requests out per request (collect_spend.add_usage), and both of its pricing paths pass the split through. - Effect: this workspace's October 2026 Claude Code spend, priced from the registry, drops from $1,094.52 to $791.65 (−27.7%), all of it from Opus 5.5 cache reads.
- Cache rates:
out_of_scopeonly on diff scans. The pinned engine rejects anout_of_scopereview-surface entry on a vuln-findings report withoutmetadata.additional.coverage_diff.
render-narrateescalates to Sonnet 5.5.claude-sonnet-5-5now comes beforeclaude-sonnet-5in its approved list, so asonnet-classescalation resolves to Sonnet 5.5, asfact-judgealready does. Its floor model (Haiku 5.5) is unchanged.
fact-judgeprefers Sonnet 5.5 (claude-sonnet-5-5first, thenclaude-sonnet-5). The price is the same, and cache reads cost half as much.render-narrateprefers Haiku 5.5 (claude-haiku-5-5first, thenclaude-haiku-4-5-20251001), at a tenth of Haiku 4.5's price. Prompts over 100k tokens bill at $0.50/$2.50, which the registry's one price per model doesn't record, so spend on long prompts is understated.- Fable 5.1 is a candidate on every
mythos-classrole (candidates: [claude-fable-5-1]). It awaits evals against Mythos 5.1 and changes no routing until it's approved.
- Model registry updated to the current Anthropic lineup (published rate
card, checked 2026-10-16).
- Mythos 5.1 replaces Mythos 5 in every role that approved it: all
eight
mythos-classroles, plus the mythos escalation entries infact-judge,fuzz-authorandvuln-scan. Mythos 5.1 is a limited-availability model, so a deployment without access to it can no longer resolve those roles. - Opus 5.5 is the preferred Opus, then Opus 5, then Opus 4.8, in every
role that approved Opus 4.8.
fuzz-authorandvuln-scannow resolve to Opus 5.5.fact-judgeandrender-narrateresolve at their own floors and reach Opus only when escalated. - New rate-card entries:
claude-mythos-5-1andclaude-fable-5-1($10/$50),claude-opus-5-5($4/$20),claude-sonnet-5-5($2/$10) andclaude-haiku-5-5($0.10/$0.50 for prompts up to 100k tokens). Older entries stay so past runs keep a price. claude-sonnet-5corrected from $3/$15 to $2/$10. Its launch price became the standard price. Spend priced from the registry for Sonnet 5 was overstated by half, and drops by a third once recomputed.- The
claude-opus-4-6note no longer calls its price empirical; it is on the published card.
- Mythos 5.1 replaces Mythos 5 in every role that approved it: all
eight
- New
vuln-scanmodel role (flooropus-class, approvedclaude-opus-4-8andclaude-mythos-5, no ledger validity)./vuln-scanresolved its model through the registry but named no role, so it ran unrouted on whatever model the session had. It now runs on, and records spend against,registry models resolve vuln-scan. The role is separate fromdeep-audit, so scan cost tunes on its own.docs/model-classes.mdanddocs/standalone-usage.mdmovevuln-scantoopus-class. - Repo-root paths after the stage-directory move.
/vuln-scan,/triage,/countersign,/threat-modeland/patchdefined<harness>as<skill-base>/../.., which since the move toharnessing/<N>-<stage>/<skill>points atharnessing/, not the repo root. Script paths built on it (for example/triage's<harness>/harnessing/4-triage/triage/scripts/...) did not exist. Now<skill-base>/../../... /vuln-scandiff mode namesscripts/resolve_baseline.pyandscripts/build_diff_packet.pyat their real paths.
- Review-surface coverage beyond the audit. traust-contracts 0.57.0 and
traust-engine 0.26.0 (pinned here) declare and check the record on the
scans that follow an audit.
/vuln-scanand/verify-remediationrecordreview_surface_coveragefor the code they reviewed. A--diffor regression-scan scope answers untouched surfacesout_of_scope. These reports have nonegative_results, so every status exceptfindingcarries a note./secure-rpm-auditrecords the record too, weighting dist-git patches and spec-shipped defaults./threat-modeltags threats and entry points withreview_surfaces.traust reporting validaterequires the record on vuln-findings, verification and rpm-profile reports from this release (0.21.0) on.
build_cumulativemerges scan coverage into*-findings-current.json. It finds scan reports through thesource.refof thevuln_scan_report/verification_reportevents it replays, and as the repo's own<repo>-vuln-findings.json/<repo>-remediation-verification.json(a scan with no new findings writes no events). Only scans dated on or after the audit apply, oldest first.- A
findingadds its IDs; a routed regression maps to itsrouted_id. - A
negativereplaces earlier answers, but never afindingentry whose findings are still open. Its note becomes anegative_resultsentry. - A scan's
out_of_scope,not_applicableandnot_assessedchange nothing. - An audit with no record (before traust 0.20.0) starts at
not_assessedwith matchingcoverage_gaps, once a scan record applies. metadata.additional.cumulative.review_surface_sourcesnames the scan that last answered each surface.- With no scan record, the output is unchanged.
countersignpasses the audit path through, so its rebuilds merge too.
- A
- The method companion
review-surfaces.mddocumentsout_of_scope, the scan records, the merge and the threat-model tags;/track-findingsanddocs/report-structure.mdlink it. /vuln-scan's diff-mode dispatch note no longer carries a deployment's pilot cost figures; dispatch stays an operator decision with no fixed cost gate.
- Pins traust-engine 0.25.2 (
87a94fc).traust reporting validatedirectory sweeps skip hidden directories (.triage-state/,.threat-model-state/and similar). The fingerprint, repository and strict checks run only on a report-shaped document: before this, a skill working file aborted the sweep for its whole directory with a traceback.
- Pins traust-contracts 0.57.0 (
4e900e7), with traust-ledger and traust-engine re-pinned to match.- Verification and vuln-scan reports may now carry a declared
review_surface_coveragerecord, andreview_surface_statusgainsout_of_scope. Threat models may tag threats and entry points withreview_surfaces.traust reporting validatechecks their shapes. The fields stay optional until the skills produce them. - Storage is at revision 4 (
verification.review_surface_coverage,threat.review_surfaces), so existing storage databases needStore.migrate().
- Verification and vuln-scan reports may now carry a declared
- Pins traust-engine 0.25.1.
traust reporting validatenow detects*-remediation-verification.json,*-refuted-register.jsonand*-priv-profile.jsonby filename. Until now each of them failed against the audit-report schema. docs/storage-v1-ddl-model.mdis regenerated from the pinned contracts DDL.
/secure-code-auditrecords review-surface coverage as data. Every code-profile report carriesreview_surface_coverage(traust-contracts 0.56.0), one entry per review surface:findingwithfinding_ids,negativewith anegative_resultsentry whoseareastarts with the surface ID,not_applicablewith a note, ornot_assessedwith a note and acoverage_gapsentry. A new pre-validation step has the auditor check the record. traust-engine 0.25.0 makes it required for code-profile audits from this release, and checks every entry against the rest of the report. A surface judged but left unrecorded now fails validation instead of reading as covered.- The surface checks live in the skill.
docs/review-surfaces.mdmoves toharnessing/3-audit/secure-code-audit/review-surfaces.md, so methodology stays out of the adopter-facingdocs/. The new "Recording coverage" section replaces the minimum-coverage and coverage-gap sections.docs/report-structure.mddocuments the field. - Pins traust-engine 0.25.0 (
bc14d6a, traust-engine#27). Its version gates now pick the numbering from the audit date, so they hold across the 2026-09-10 version reset.
- Pins traust-contracts 0.56.0 (
cb8bba8), with traust-ledger and traust-engine re-pinned to match. Reports may now carry the declaredreview_surface_coveragerecord.traust reporting validatechecks its shape (all six review surfaces, valid statuses,finding_ids/notewhere required). The field stays optional until/secure-code-auditproduces it. Storage is at revision 3 (report.review_surface_coverage): existing storage databases needStore.migrate(). docs/storage-v1-ddl-model.mdis regenerated from the pinned contracts DDL. It had fallen behind earlier storage releases (43 → 49 tables).
- New
docs/review-surfaces.md: six code surfaces every review checks. These arecredential-acceptance,ownership-resolution,security-flags,outbound-fetch,stored-content-integrityandscope-enforcement, each with the check it requires, itscategoryand CWEs, and what a scoped negative result looks like. It is the single definition; skills reference it. /secure-code-audit:- Review-depth heuristic 5 checks the six surfaces on every audit.
- With no threat model, they are the minimum coverage: each applicable surface ends with a finding or a scoped negative result.
- A scoped or partial audit records surfaces it skipped in
metadata.additional.coverage_gaps(tool: "review-surfaces"). - A search-based negative result quotes the exact pattern searched.
- PEACH data segmentation adds the stored-content integrity check.
- Scanner suppression comments in the target (
gitleaks:allow,nosemgrep/nosem,# nosec) are named as repository content, never exemptions, in the adversarial-content doctrine and the skill. The pre-scans already disable them and flag the candidates.
- Pins traust-engine 0.24.0:
traust adapters opengrepno longer honours the scanned target's ownnosemgrep/nosemcomments. A fact on such a line is reported withnosem_marked: trueinstead of being dropped, the same treatmenttraust adapters gitleaksgivesgitleaks:allow(inline_allow).
- One copy of each rule pack, loaded with no configuration. The opengrep
rule pack and the gitleaks config live only in traust-engine
(
traust_engine/data/secure-code-audit/), which is what every default run already loaded. The harness copies underharnessing/3-audit/secure-code-audit/are deleted. Rule pack v1.8 (from 0.17.0) only reached default runs once traust-engine 0.23.0 carried it, so this release pins traust-engine 0.23.0 (80bf320). - New docs gate
single rule pack: fails on a rule-pack directory in this repo, or on a skill, doc or config that points at the removed copies. - New
tests/test_default_rule_packs.py: with the adopter-style fixture config (no rule-pack location set),traust adapters opengrepreports a v1.8 rule andtraust adapters gitleaksreports a Traust detector. Not markedintegration, so the pre-push hook runs them wherever the scanners are installed. /secure-code-audit,/mine-ledgerand the docs point at the bundled packs. Rule promotion from/mine-ledgerdrafts is a traust-engine PR, followed by the traust engine-pin bump.
- Pins traust-contracts 0.55.0, traust-ledger and traust-engine 0.22.1.
- traust-contracts 0.55.0 types the run records under report
metadata.additional(traust-contracts#42). A report that writes one of them in another shape, such asnullinsidemodel_routing, now failstraust reporting validate. It also brings storage revision 2 from 0.54.0. - traust-engine 0.22.1:
traust registry models stampleaves outfloorfor a role missing from the registry instead of writingnull; the gitleaks adapter (0.22.0) no longer honours the scanned target's owngitleaks:allowcomments and flags those candidatesinline_allow.
- traust-contracts 0.55.0 types the run records under report
- Rule pack v1.8: authentication, ownership and crypto-hygiene
rules. Seven new or widened opengrep rules for defect classes that
review missed and no rule surfaced (see the pack README for each rule's
shape and exclusions):
traust-python-injection-like-wildcard-unescapedandtraust-go-injection-like-wildcard-unescaped(CWE-155): a LIKE/ILIKE pattern built from a run-time value with%/_left live, through an ORM pattern operator, raw SQL text, or a wildcard-concatenated bound argument. Escape helpers and ESCAPE clauses sanitize; DDL(LIKE table …)is excluded.traust-python-authentication-jwt-signature-unverified(newpython/authentication.yaml) andtraust-go-authentication-jwt-signature-unverified(CWE-347): token decoding with signature verification off, including the conditional shape (options mutated under a test/debug flag),verify=False, thenonealgorithm, Go key functions that return no key, andParseUnverified.traust-python-input-validation-pickle-loads-taintnow also sinks onUnpickler(...).load()(including subclasses),dill,jsonpickleandshelve. The newtraust-python-input-validation-restricted-unpickler-inventory(INFO) lists everyUnpicklersubclass overridingfind_class, so a negative result cannot report deserialization absent where one exists.traust-python-cryptography-secret-compare-not-constant-timeandtraust-go-cryptography-secret-compare-not-constant-time(CWE-208, LOW confidence):==/!=/bytes.Equalon secret-named operands, excluding literal, nil, error-sentinel, enum-constant and metadata-named comparisons.
- The rules are authored in the harness pack
(
harnessing/3-audit/secure-code-audit/opengrep-rules/). A run uses them whenlocations.opengrep_rules(or--rule-pack) points at that directory; the copy bundled in traust-engine is unchanged by this release.
- The route×guard pre-scan enumerates Flask.
enumerate_route_guards.pygains aflaskfamily, parsed withast(target code is never imported):@<app|blueprint>.routeand verb decorators,add_url_rule()including MethodView/View.as_view()classes (one row per HTTP method the class defines), and Flask-RESTful / flask-restxadd_resource()and@<api|namespace>.routeresource classes. Guards come from handler and method decorators, classdecorators/method_decorators(inherited through in-repo base classes) andbefore_request/before_app_requesthooks that can reject a request. A blueprint hook covers that blueprint; an app hook covers the same app object and the blueprints its file registers, never every route in the tree. Explicit public / exempt decorators are unguard markers. - Flask rows add a second asymmetry shape: a guard family (scope, admin,
role, permission, csrf, token, authn) carried by most rows of one view
class or blueprint but missing from a guarded sibling marks that sibling
judgement_requiredwithguard_family_gaps. Computed paths are kept as source text withpattern_dynamic: true(module-level string constants resolve to their literal), and rows under a test/example directory carrytest_path: true. - A tree that imports Flask but has no recognizable registration reports a
flaskcoverage gap rather than reading as clean; Flask is no longer a coverage gap when it was enumerated. Files that also import FastAPI never contribute verb-decorator rows. secure-code-auditSKILL.md anddocs/language-support.mdlist the new family.
- The docs gate accepts the
docs/README.mdindex. Since the README rewrite (#68), the root README links one reader's map,docs/README.md, instead of listing every guide, and the enumeration-coverage check failed for 24 pages. Adocs/page now counts as reachable when the README or the index mentions it, and a new check fails if the README stops linking the index, so every page stays one hop from the README. docs/cli-reference.mdlistsledger pull.
- The ledger is configured in
ledger.yaml, not environment variables. New templatesconfig/ledger.example.yamlandconfig/storage.example.yaml(traust-contracts 0.53.0 shape:dsn,signing,identity). They are opt-in:install_traustlists them but never copies them, since an example DSN would switch the deployment to a database that does not exist. - With
ledger.yamldsnset, the Ledger database is the authority for disposition layers and*-findings-layer.jsonbecomes a read-only snapshot, so countersigns written outside the harness (e.g. in SCI) reach the next run. Every ledger writer —emit-triage,emit-validation,emit-verification,route_regressions,route_impact_findings,build_cumulative,ledger pull,corpus finding-identity rebaseline, the fuzz emitter andbaseline_claims— takes--ledger-layer/--product-repoand binds the run's layer once (traust-engine 0.21.0engine.ledger.bind); the emitters forward it to thebuild_cumulativethey spawn, andcountersignreads the layer through the ledger service. Without adsn, file mode is unchanged. Read-only sweeps (build_verify_sweep, the fuzz cross-layer fingerprint lookup) read layer files, which are the snapshots in database mode. - New
traust ledger pull <layer>exports the bound layer as that snapshot before an LLM session. build_cumulativereads the layer through the ledger service, so its claim check and cumulative report see the database layer, not a stale file.- Breaking: signing and the employee directory come from
ledger.yaml. With traust-engine 0.21.0 noLAAS_SIGNING_*orLEDGER_DIRECTORY_COMMANDvariable configures a harness run: setsigning(method, key path,required) andidentity.directory_commandin the mountedledger.yaml.countersignandcompliance_scope_intaketake their up-front directory check from the same configured directory. Docs (signing.md,disposition-ledger.md,setup.md,architecture.md,requirements.md, skills) describe the config keys instead of the variables; only the standaloneledgerCLI still reads its ownLAAS_SIGNING_*. - New
ledger-dbextra (traust-ledger[db]) for the database backend. - Pins traust-engine 0.21.0 (traust-security/traust-engine#21), traust-ledger
0.11.1 and traust-contracts 0.53.0, all from
traust-security. docs/report-structure.mdlists everyremediation_effortvalue.
- The scheduled threat-model check-up applies OWASP ratings. A new
/threat-model review --auto --apply-ratingsmode (review.md R6) writes the OWASP Risk Rating Methodology ratings it proposes for unrated threats and leaves every other proposal report-only for a human. The quarterly and release-review lanes now dispatch it;--autoalone stays report-only. Release-branch models are still outside those lanes, by design. traust reporting renderwon't erase threat-model prose. It refuses to overwrite an existing.mdwhen the new rendering would drop or empty a section the file has, which is what happens when the JSON lacks members the Markdown carries (the pre-schema backfill). Pass--allow-section-lossfor an intended removal. Under--apply-ratingsa refusal means "re-model needed" and nothing is written.
- traust-engine 0.18.2. Rendering a threat model from its JSON is now lossless: the update history sits inside section 7 (so a rated model with history passes lint), optional asset columns are kept, and attack scenarios render as prose.
- traust-contracts 0.47.0 (every threat-model section defined) and traust-ledger 0.8.5.
- traust-contracts v0.46.0 (OWASP risk ratings in the storage
threattable), traust-ledger 0.8.4 and traust-engine 0.18.1. Nothing else changes.
- Released pins. traust-engine
v0.18.0and traust-ledgerv0.8.3replace the fork commits 0.13.0 merged with. They are the same trees, so nothing else changes.
- Threats are rated with the OWASP Risk Rating Methodology. This
replaces the impact/likelihood labels and 1-2-4-8-16 weights inherited
from the upstream skill, which weren't a published standard. Pins
traust-contracts v0.45.0 (
risk_ratingon threats), traust-ledger 0.8.3 and traust-engine 0.18.0; the engine and ledger are pinned to their open-PR commits until they're tagged./threat-model: the schema.md scoring guide restates the method in original wording, citing OWASP. The author scores the sixteen 0-9 factors with a reason for each one that evidence, a control or the owner moved. Bootstrap rates on the technical basis; interview rates on the business basis when the owner can answer. Section 4 gainsseverity | likelihood | impact, and a new section 11 lists the factors. bootstrap, interview, review, update and pr now speak the new vocabulary.- New
traust reporting rate-threats <model>.jsonfills each rating's scores, levels and severity from its factors (--checkreports without writing).validaterejects a rating that disagrees with its factors. - Existing models move over at their next update: any pass that writes a model rates every threat still carrying legacy labels (review R3b, update U1). Nothing is re-rated now.
/threat-registerranks by OWASP severity, then impact and likelihood score. It orders legacy threats by a labelled crosswalk (never reported as a rating) and orders products by open-severity counts rather than a weighted sum. It reports how many threats are rated.score,score_sum,max_scoreandopen_critical_plusare replaced byseverity,severity_source,open_by_severity,max_severityandopen_critical. Quick wins now cover unmitigated threats of high or critical severity.- Crown-jewel tiers: a rated threat's impact component is its OWASP impact score / 9; legacy threats keep their points / 5.
docs/external-dependencies.mdrecords the OWASP Risk Rating Methodology (CC-BY-SA-4.0, reimplemented in original wording), anddocs/risk-rating-methodology.mdcovers threats.
- Pins: contracts 0.44.0, engine 0.17.0, ledger 0.8.2. Ledger stays a
declared dependency until the four direct
traust_ledgercall sites go through the engine'sLedgerService. Ledger ≥0.7 verifies the caller's identity on every write, so running the harness needs a real identity: OIDC (LEDGER_OIDC_ISSUER/LEDGER_OIDC_JWKS_URL) or local auth (LEDGER_LOCAL_IDENTITY, orledger auth local). - A rebaseline is now a ledger restatement.
baseline_claims record --rebaseline <id>needs--ticketand--rationale, and writes the new claim hash throughLedgerService.restate(), which records the prior hash, the verified actor, the ticket and the rationale in the layer. When the report bytes changed too,audit_report_sha256is restated with it. Pinning a new finding's hash is unchanged. On ledger 0.8 the old path, an unrecorded overwrite, is refused.track-findingsanddocs/disposition-ledger.mddescribe the new flags; the ticket and rationale are the user's to supply. - The test suite sets up its own local ledger identity.
tests/conftest.pyisolatesHOME, clears inherited ledger tokens, setsLEDGER_LOCAL_IDENTITY=test@traust.local, and stops the run if auth doesn't resolve. Tests no longer pass placeholder tokens. Therequires_ledgermarker and itsLAAS_TOKENskip are gone, so the 17 ledger integration tests it hid now run; fixing them turned up the two fixes below. See CONTRIBUTING.md → "Ledger auth in tests". - Fix:
emit_triage_ledger_eventsreported every idempotent re-run as "N appended, 0 already present". An empty list of new event IDs was treated as unknown; the sibling emitters already handled it. - Fix: tests that drove
build_cumulative/emit_triage_ledger_eventsfrom a temp directory now pass--findings-root, and thesweeptest creates its duplicate symlink the right way round. The path confinement is unchanged and still has its own tests. remediate-findingcites its manifest and mutation-evidence scripts by their fullharnessing/paths, so the reference-integrity check passes again.config/locations.example.yamllists every key with an empty default and documentss3://foranalysis_results(credentials come from the environment, never the file).
- Stage-7 patch evidence is findable from
/patch.remediate-findinggains an## Integrationssection naming its mutation (Phase 4b) and property (Phase 4c) evidence producers and the stage-8scanner_differential;/patchpoints to all three; the skill's heading said Stage 9 and now says Stage 7.docs/artifacts.mdno longer claims/verify-remediationreads*-remediation.json.
- Reverted the 0.10.0 findings.db and census changes. Every consumer
reads the previous nine-table
findings.dbprojection again and the census walks reports as before. Pins: contracts 0.35.0 (the 0.33.0 storage contract), engine 0.16.0, ledger 0.6.32. 0.10.0 remains tagged and should not be pinned. Rebuildfindings.dbwithtraust corpus findings-dbafter upgrading; a store built by 0.10.0 is refused.
-
The reachability graphs now say what they are for, not just what they are. Their purpose is deciding whether an advisory reaches your code: a manifest pin proves a vulnerable package is present, not that anything calls the vulnerable function.
/impact-analysisuses the answer as its strongest evidence tier, and/secure-rpm-audituses it too.Documents the asymmetry that makes the tier trustworthy: a resolved first-party call promotes to evidence
symboland classificationaffected, package-level calls promotemanifest→symbol-usage, and an absent call path never demotes anything — DI, reflection and MethodHandles hide edges from static analysis, sono_call_sites_foundis recorded honestly. Also why the tier is gated on cheaper tiers (a CPG is expensive) and that an absent Joern recordsskipped, never a fake pass.
-
findings.dbremoved from the graph list. It is a relational projection of the findings corpus, not a graph — the page even said so while listing it. It appears now under an explicit "Not a graph" heading, because its location inanalysis-results/graph/invites the assumption. One table inside it,graph_edges, is imported from repo-graph so findings can be joined to what ships them; a borrowed edge table does not make the database a graph. -
Joern is described as what it is: a Code Property Graph. AST, control flow and data flow in one queryable graph, built by
joern-parsewith the frontend named per language and queried with CPGQL. Calling it a "call graph" undersold it. Still transient — built in a temp dir and discarded with the run — so it and the govulncheck call graph are the richest graphs the harness touches and the only ones that cannot be queried afterwards.
-
docs/graphs.mdnow covers every graph the harness builds or reads, not two of them. Added:findings.db(the findings projection), the ATT&CK Navigator coverage layer and how its observed/modeled/derived scores differ, per-findingattack_chains[], and the transient reachability call graphs built byadapters govulncheck/adapters joern— which are analysis steps with nothing persisted, worth saying so nobody looks for an artifact.Retitled: calling both graphs "portfolio graphs" reproduced the exact confusion the page exists to remove. One is
repo-graph, one isportfolio-graph.Also dropped the doc's opening banner, which described how the page was researched rather than telling a reader anything.
-
docs/is adopter-facing; estate specifics removed.docs/graphs.mdcarried one deployment's graph sizes and node counts, anddocs/model-classes.mdcarried development history. Neither is product documentation. Sizes are now stated as deployment-specific with a pointer torepo-graph-stats.md, which reports the reader's own. -
New docs-consistency check,
docs/ estate neutrality. The deployment vocabulary was applied toconfig/but never todocs/, which is how those specifics passed every gate. The same block/review rules now run overdocs/**/*.md. All 32 existing docs pass unchanged; mutation-tested by injecting a vocabulary match.Note the limit: estate-specific figures — graph sizes, corpus counts — match no pattern and stay a review concern. Write them as deployment-specific rather than quoting one deployment's numbers.
-
New
docs/graphs.md— how/repo-graphand/portfolio-graphare actually used, which was documented nowhere. The two SKILL.md files were the only substantive source; every mention acrossdocs/was a one-line passing reference with no section.Covers what each graph holds and the question it answers, the L0–L4 layer model, that
repo-graph.jsonis/portfolio-graph's L0 spine (so it is a prerequisite, not an alternative), the full consumer list for each artifact, and what a stale graph gets wrong — a stale repo-graph misstates coverage and ownership, a stale portfolio-graph understates blast radius, which reads as good news. -
/portfolio-graphgains the## Integrationssection it was missing./repo-graphhad one; the skill holding the code-level graph documented neither its consumers nor what depends on it being fresh. -
New docs-consistency check,
graph consumer lists. The consumer lists were built by searching for the artifact filenames, so they rot as soon as a new consumer lands. The check fails when anything opensportfolio-graph.dborrepo-graph.jsonwithout being named in the doc. Mutation-tested with a throwaway module, and it caught two real cases while being written: itself (it holds the artifact names as data) andcheck_drift, which is documented under its skill name/drift-watch.
-
Documents how an adopter configures their own models, which was the one question
model-classes.mdcould not answer.model-registry.yamlis a seeded shipped default and the copy in$TRAUST_CONFIG_HOMEwins, but that was a single clause inmodel-routing.mdand said nothing about how.New "Configuring your own models" section covers the per-role granularity (repoint one role, the rest keep their defaults), the exact schema requirements at provider/model/role level, that no vendor is assumed, that
validaterefuses a sub-floor approval, and whatinstall_traust --doctor/--forcedo to a deliberate override. Linked frommodel-routing.md,requirements.md(which now says outright that the classes are defaults, not requirements) andconfig/README.md.Every claim was executed against a scratch config home, which caught the worked example being wrong: replacing a role block drops
candidatesandledger_validity_writer, both schema-required, so the YAML an adopter would have copied failed validation. The example now changes only theapprovedline.
-
New
docs/model-classes.md, linked fromrequirements.mdandmodel-routing.md: what each of the four tier classes is for, the eleven role floors, which roles may write a validity verdict into the ledger, the escalation triggers, and a per-skill class index.Two things it clears up, because both were causing the question "where is this documented" to have no good answer. The per-skill classes live in two tables —
campaign-workflow.md(21 pipeline skills) andstandalone-usage.md(27 standalone skills) — which are complementary, not duplicates, so neither reads as authoritative. And skill frontmatter'sharness.tier(primary/secondary/tertiary/ci) is a different axis entirely, which makes it look like the answer when it is not. -
New docs-consistency check,
model-class index. The index is derived from the two usage tables and nothing links a skill to a class mechanically, so the check fails on a misclassed skill, a name that is not a skill, an indexed skill with no class in either table, or a classed skill missing from the index. Mutation-tested both ways.
-
Pin traust-contracts v0.5.0, traust-engine v0.2.5 and traust-ledger v0.3.0. Brings in the
evidenceprojection column (so typed patch evidence is queryable in SQL, not just present in the payload), the fixedtraust_storagePostgreSQL namespace, and the ledger's stamp/whoami REST verbs.Smoke-tested against the pinned copy rather than assumed: the proof-requires-both-observations rule still holds, the verification family still carries
evidence, and both projection tables now report anevidencecolumn from the pinned DDL.
-
Point every sibling pin at the new
traust-securityGitHub organisation: traust-contracts v0.4.0, traust-engine v0.2.4, traust-ledger v0.2.3.The org move could not be a URL edit. uv honours
[tool.uv.sources]inside git dependencies, so a tag cut before the move carries the old URL and resolution fails with "conflicting URLs for package traust-contracts". The corrected URL only reaches consumers as a new tag, making this the same bottom-up train a schema change needs: ledger 0.2.3, engine 0.2.4, then here.
-
The execution boundary is a config setting, not an invented env var. 0.2.3 added
TRAUST_SANDBOXED_RUNNER, a variable nothing on the platform exports — a convention only this repo knew about, so the path it guarded would never have fired. Replaced bysandbox:in$TRAUST_CONFIG_HOME/execution-boundaries.yaml, with a template inconfig/seeded byinstall_traust.sandbox: noneis the default: direct execution with a scrubbed environment, on a disposable copy rather than the worktree the diff comes from.sandbox: podmanopts into a nested rootless container and is never silently downgraded — with no working runtime the lane reportsnot_attempted. Detection stayspodman inforather thancommand -v podman.Verified on both lanes in both modes: default runs and returns
proves(mutation 19/19; property failed-then-passed), a configuredpodmanwith no runtime refuses with that reason, and an invalid value falls back to the documented default. Documented in setup.md, config/README.md, external-dependencies.md and the orchestrator prerequisites.
-
The evidence lanes no longer hard-require podman.
run_mutation.shandrun_property.shaccept either nested podman or a platform-attested sandbox, declared by the orchestrator asTRAUST_SANDBOXED_RUNNER=<runner-label>. On a central runner whose image carries the toolchain but cannot nest containers, both lanes would otherwise have returnednot_attemptedon every run, permanently.Still no unattested fallback: with neither boundary the lanes emit
not_attemptedrather than running target code in the open. The env var is a declaration of deployment fact, not a security control — its only job is to stop podman's absence from being read as permission. -
Boundary detection uses
podman info, notcommand -v podman. The client binary is present on a host whose VM is stopped and in images that ship the CLI with no runtime; choosing podman there failed every run with exit 125 — the exact failure the change was meant to prevent. Caught by running it with the VM deliberately stopped. -
The resolved boundary is recorded in each evidence item's
command, since a verdict from nested podman and one from an attested sandbox are not equally strong.docs/external-dependencies.mdgains the boundary contract and names the new podman consumers;docs/continuous-operations.mdgains the orchestrator prerequisite row.
-
Pin traust-contracts v0.4.0 / traust-engine v0.2.3 / traust-ledger v0.2.2, so a verification report can carry the typed patch-evidence block.
-
/verify-remediationgains its one executed claim (ToB plan item 5b). Step 4-pre already re-ran the scanner on the patched checkout and compared against facts recorded in the original report — which is why it warned that a vanished fact can be rule evolution rather than a code change. New step 4-pre.5 scans BOTH revisions and emits a typedscanner_differentialevidence item, making the rule-evolution case an explicitnot_attemptedinstead of a silent mis-read.§8a's stage-8 ceiling is lifted for that differential only:
provesmeans the pattern the finding rested on is gone — pattern-level, since a diff can silence a scanner by moving the sink. Findings with no scanner backing stay analysis-only, and step 4a's root-cause check remains mandatory.
-
Pin traust-contracts v0.3.0, traust-engine v0.2.2 and traust-ledger v0.2.1 — the final hop of the train for the remediation
evidence[]block. Until this pin,reporting/validate.pyrejected any report carrying it, because the remediation schema isadditionalProperties: false.Smoke-tested rather than assumed: the same evidence-carrying report validates with 0 errors against the newly pinned schema and is rejected against v0.1.1 with "Additional properties are not allowed ('evidence' was unexpected)", and all eight enforcement cases behave — a
provesclaim missing either observation is refused,not_attemptedwithout a reason is refused, and invented kinds/outcomes are refused.
-
Event
occurred_atis built through contracts'to_rfc3339viatraust.lib.event_time.report_occurred_at, replacing the same f-string in four emitters. A report stating an unusable date now raises instead of getting a silent substitute; an absent one falls back torecorded_at. A triage report carrying"triage_completed": truepreviously emitted the literal'TrueT00:00:00+00:00'. -
--recorded-atis validated at the CLI boundary in all six producers. It fedrecorded_atunchecked, whose[:10]prefix reaches interactivesource.refand thereforeevent_id. -
New migration
traust.migrations.fix_event_timestamps: converts bare dates to midnight UTC (preserving the[:10]prefix, soevent_iddoes not move), drops an unrepairableoccurred_at, and reports a non-conformingrecorded_atrather than rewriting a required field. Dry-run by default, idempotent, and a layer that arrives signed must leave signed. -
Read sites prefer a parseable timestamp over a merely present one.
build_trends.pybucketed viadate.fromisoformat(value[:10])and crashed on'TrueT00:00'. -
Unrelated fix, bundled:
check_reference_integrityallowlisted only<skill_dir>/scripts/while the docs use<skill-base>, so it flagged a valid reference as stale and failed on a clean tree. It also missed a genuinely stale path in the same file. Both corrected.
Pins move to contracts / engine / ledger 0.1.1, which enforce RFC 3339 on
LayerEvent.recorded_at and .occurred_at on read as well as write. Run the
migration against any existing corpus first:
python3 -m traust.migrations.fix_event_timestamps <results-root> # dry run
python3 -m traust.migrations.fix_event_timestamps <results-root> --apply # write--apply re-roots each repaired layer, so it needs a signing identity
(LAAS_SIGNING_KEY_PATH + COSIGN_PASSWORD) for layers that are currently
signed. Unsigned layers only need re-stamping.
Workflow engine for automated, multi-framework security assessment of software portfolios — source repositories, container images, RPM packages, Kubernetes operators, and infrastructure-as-code. Provides the skills, slash commands, schemas, and prompt engineering that let an AI coding agent run consistent, repeatable audits, then triage and validate findings, recording every disposition in a signed ledger.