Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .ratchet.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
{
"harnessing/3-audit/pqc-readiness/scripts/pqc_facts.py": {
"contracts": 1
},
"src/traust/cli/auth.py": {
"core-internal": 1
},
Expand All @@ -13,7 +10,6 @@
"database": 1
},
"src/traust/cli/check_docs_consistency.py": {
"contracts": 1,
"state-file": 2
},
"src/traust/cli/check_skill_alignment.py": {
Expand Down
9 changes: 5 additions & 4 deletions harnessing/3-audit/pqc-readiness/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ If facts already exist (`--from-facts`), load them. Otherwise run the
deterministic scanner:

```bash
syft dir:<checkout> -o cyclonedx-json > <slug>-sbom.json
python3 <skill-base>/scripts/pqc_facts.py \
--repo-dir <checkout> --repo-url <URL> \
--repo-dir <checkout> --repo-url <URL> --sha <commit> --sbom <slug>-sbom.json \
--out <slug>-pqc-facts.json --cbom-out <slug>-cbom.json
traust pqc record --repo-url <URL> --sha <commit> --facts <slug>-pqc-facts.json
traust pqc prepare --repo-url <URL> --sha <commit>
Expand Down Expand Up @@ -562,8 +563,8 @@ anything uncertain `posture: unknown`.
## Two-layer probe model

```
Layer 1 (census): python3 -m traust.cli adapters crypto-source → CRYPTO_* facts
↓ imported by
Layer 1 (census): python3 -m traust.cli adapters crypto-source → CRYPTO_* facts (traust-core census)
↓ read by
pqc_facts.py → HP_CHAIN_* PQC facts + pqc_capable flag

Layer 2 (TLS/code): pqc-scan rules (opengrep) → CWE/algorithm-level crypto usage
Expand All @@ -576,7 +577,7 @@ Chain reasoning: crypto-analysis SKILL.md → governance chains (who owns
| ----------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `crypto-source` | python3 -m traust.cli adapters crypto-source | Provider census: Go, Node, JDK, Python, Rust, .NET, Ruby, C/C++ crypto stacks, base images, RPM locks, GODEBUG, crypto-policies |
| `crypto-cluster` | python3 -m traust.cli adapters crypto-cluster | Unopinionated data collector — `cluster` tier with `--groups` provides raw TLS negotiation facts for PQC probing |
| `pqc_facts.py` | `harnessing/3-audit/pqc-readiness/scripts/pqc_facts.py` | Imports crypto*probe, maps `CRYPTO*_`→`HP*CHAIN*_`, adds PQC capability assessment via `\_pqc_capable_version()`(reads thresholds from[`notes/reference/pqc-version-matrix.yaml`](notes/reference/pqc-version-matrix.yaml)) |
| `pqc_facts.py` | `harnessing/3-audit/pqc-readiness/scripts/pqc_facts.py` | Reads traust-core's crypto census of the commit (checkout + manifest SBOM), maps `CRYPTO*_`→`HP*CHAIN*_`, adds PQC capability assessment via `\_pqc_capable_version()`(reads thresholds from[`notes/reference/pqc-version-matrix.yaml`](notes/reference/pqc-version-matrix.yaml)) |
| `scan_xcrypto_usage.py` | `harnessing/3-audit/pqc-readiness/scripts/scan_xcrypto_usage.py` | golang.org/x/crypto evidence: first-party import sites, go.mod versions, optional callgraph reachability (somepath witness) |
| `traust pqc blockers` | traust-core `PqcService.blockers` | Derives each recorded report's `remediations[]` in the findings vocabulary (severity, classification, `patchable`) for `/patch` and other findings consumers |
| `novel.py` | `harnessing/5-validate/validate-findings/novel.py` | Runtime probes: `pqc-tls-negotiation`, `pqc-cert-algorithm`, `pqc-crypto-policy`, `pqc-backend-tls` |
Expand Down
95 changes: 64 additions & 31 deletions harnessing/3-audit/pqc-readiness/scripts/pqc_facts.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@
`PqcService.prescan` replaces it; record its output with `traust pqc record --facts`.

Usage:
pqc_facts.py --repo-dir DIR --repo-url URL --out FACTS.json \
[--scanner BIN] [--rules-dir DIR] [--cbom-out CBOM.json]
pqc_facts.py --from-sbom SBOM.json --repo-url URL --out FACTS.json
pqc_facts.py --repo-dir DIR --repo-url URL --sha SHA --out FACTS.json \
[--sbom SBOM.json] [--scanner BIN] [--rules-dir DIR] [--cbom-out CBOM.json]
pqc_facts.py --from-sbom SBOM.json --repo-url URL --sha SHA --out FACTS.json

The commit's manifest SBOM (`syft dir:<checkout> -o cyclonedx-json`) is recorded first:
`--sbom` in source mode (unless already recorded), `--from-sbom` alone in SBOM mode.
"""

from __future__ import annotations
Expand All @@ -41,17 +44,20 @@
from typing import Any

import yaml # PyYAML — already in harness deps
from traust_contracts.paths import schema_path
from traust_core.v1.models.code_facts import CryptoFact, CryptoProbeId
from traust_core.v1.models.common import ToolRequest
from traust_core.v1.models.dependencies import NoManifestSbomError
from traust_core.v1.models.values import HttpsRepoUrl

from traust.cli.groups._registry import load_engine # wave 2 lane (e)
from traust.context import add_config_home_arg
from traust.cli._doors import register_commit
from traust.context import add_config_home_arg, load
from traust.paths import schema_path

SCRIPT_DIR = Path(__file__).resolve().parent
SKILL_DIR = SCRIPT_DIR.parent
HERE = SKILL_DIR
ADAPTER_VERSION = "1.4.0" # 1.4.0: HP_TLS_LISTENER_* census rules; HP_GOV_* facts first-party only
PQC_SCAN_COMMIT = "be5c760adb7d1d4d99e39d35d87d2956079db76e"
from traust_engine.adapters.crypto_probe import * # noqa: F403, E402

VENDOR_RX = re.compile(
r"(^|/)(vendor|node_modules|third_party|_vendor|3rdparty|third-party|"
Expand Down Expand Up @@ -164,16 +170,17 @@ class Coverage:
no_crypto_detected_assertion: str | None = None

def to_dict(self) -> dict[str, Any]:
d: dict[str, Any] = {"assessment_basis": self.assessment_basis}
d: dict[str, Any] = {
"assessment_basis": self.assessment_basis,
"scanned_files": self.scanned_files,
"skipped_files": self.skipped_files,
"rules_in_pack": self.rules_in_pack,
}
if self.assessment_basis == "sbom-only":
d["sbom"] = self.sbom
d["components_seen"] = self.components_seen
else:
d["scanned_files"] = self.scanned_files
d["skipped_files"] = self.skipped_files
d["rules_in_pack"] = self.rules_in_pack
if self.no_crypto_detected_assertion:
d["no_crypto_detected_assertion"] = self.no_crypto_detected_assertion
elif self.no_crypto_detected_assertion:
d["no_crypto_detected_assertion"] = self.no_crypto_detected_assertion
return d


Expand Down Expand Up @@ -424,10 +431,35 @@ def _parse_scanner_findings(report: dict) -> list[Fact]:
return facts


def _adapter_native_facts(repo: Path, tables: dict) -> list[Fact]:
"""Provider / policy / kill-switch / material facts via crypto_probe."""
def crypto_census(args: argparse.Namespace) -> list[CryptoFact]:
"""The commit's crypto census from traust-core, recording the repository, the commit and
its manifest SBOM: the checkout's configured stacks and the SBOM's crypto packages, or the
packages alone (`--from-sbom`)."""
core = load(args.config_home)
services = core.services()
url = HttpsRepoUrl.parse(args.repo_url, core.config.forge_hosts)
repo = services.portfolio.register_repository(url, args.public)
ref = register_commit(services, repo.id, args.sha, None)
assert ref.id is not None
sbom = args.from_sbom or args.sbom
if sbom:
services.exposure.ingest_sbom(Path(sbom).read_bytes(), ref.id)
try:
if args.from_sbom:
return services.code_facts.packages(ref.id)
census = services.code_facts.crypto(ref.id, ToolRequest(workspace=Path(args.repo_dir)))
except NoManifestSbomError as e:
raise SystemExit(
f"pqc_facts: commit {args.sha} has no manifest SBOM recorded: run "
f"`syft dir:{args.repo_dir} -o cyclonedx-json` and pass it as --sbom"
) from e
# A source scan reads the packages its ecosystems name; keyword components are SBOM-mode only.
return [f for f in census if f.probe_id != CryptoProbeId.SBOM_COMPONENT]


def _adapter_native_facts(repo: Path, general_facts: list[CryptoFact], tables: dict) -> list[Fact]:
"""Provider / policy / kill-switch / material facts from the crypto census."""
cap = tables["provider_capability"]
general_facts = crypto_probe.probe_repo(repo) # noqa: F405
facts: list[Fact] = []

fips_mode = any(
Expand All @@ -442,10 +474,11 @@ def _adapter_native_facts(repo: Path, tables: dict) -> list[Fact]:
if not rule_id:
continue

at = gf.location
fact = Fact(
rule_id=rule_id,
file=gf.file,
line=gf.line,
file="" if at is None else str(at.path),
line=None if at is None else at.line,
detail=gf.detail[:200],
provider=gf.provider or None,
pqc_capable=_pqc_capable_version(gf.provider, gf.version),
Expand Down Expand Up @@ -499,14 +532,12 @@ def _adapter_native_facts(repo: Path, tables: dict) -> list[Fact]:
return facts


def _sbom_facts(sbom_path: Path, tables: dict) -> list[Fact]:
"""--from-sbom mode: provider + delegated facts from a CycloneDX SBOM."""
general_facts = crypto_probe.probe_sbom(sbom_path) # noqa: F405
def _sbom_facts(general_facts: list[CryptoFact], tables: dict) -> list[Fact]:
"""--from-sbom mode: provider + delegated facts from the SBOM's crypto packages."""
return [
Fact(
rule_id="HP_SBOM_CRYPTO_COMPONENT",
file=gf.file,
line=gf.line,
file="",
detail=gf.detail[:200],
pqc_capable=_pqc_capable_version(gf.provider, gf.version),
)
Expand Down Expand Up @@ -560,7 +591,7 @@ def _build_summary(facts: list[Fact]) -> dict[str, dict[str, int]]:
# ---------------------------------------------------------------------------


def build_facts(args) -> FactsDocument:
def build_facts(args, census: list[CryptoFact]) -> FactsDocument:
tables = load_tables()
rules_dir = Path(args.rules_dir or (HERE / "rules"))
stamps = {
Expand All @@ -570,7 +601,7 @@ def build_facts(args) -> FactsDocument:
}

if args.from_sbom:
facts = _sbom_facts(Path(args.from_sbom), tables)
facts = _sbom_facts(census, tables)
coverage = Coverage(
assessment_basis="sbom-only",
sbom=str(args.from_sbom),
Expand All @@ -590,7 +621,7 @@ def build_facts(args) -> FactsDocument:

raw_summary = report.get("summary") or {}
facts = _parse_scanner_findings(report)
facts.extend(_adapter_native_facts(repo, tables))
facts.extend(_adapter_native_facts(repo, census, tables))

n_rules = sum(
len(re.findall(r"(?m)^- id:", p.read_text())) for p in sorted(rules_dir.glob("*.yml"))
Expand Down Expand Up @@ -659,6 +690,9 @@ def main():
add_config_home_arg(ap)
ap.add_argument("--repo-dir")
ap.add_argument("--repo-url", default=None)
ap.add_argument("--sha", help="the checked-out commit")
ap.add_argument("--public", action="store_true", help="the repository is public")
ap.add_argument("--sbom", help="the commit's manifest SBOM (syft dir:<checkout>)")
ap.add_argument("--out")
ap.add_argument("--scanner")
ap.add_argument("--rules-dir")
Expand All @@ -671,11 +705,10 @@ def main():
if args.validate_facts:
p = Path(args.validate_facts)
raise SystemExit(min(validate_facts(json.loads(p.read_text()), str(p)), 1))
if not args.out or not (args.repo_dir or args.from_sbom):
ap.error("need --out and one of --repo-dir/--from-sbom")
load_engine(args.config_home)
if not (args.out and args.repo_url and args.sha) or not (args.repo_dir or args.from_sbom):
ap.error("need --out, --repo-url, --sha and one of --repo-dir/--from-sbom")

doc = build_facts(args)
doc = build_facts(args, crypto_census(args))
serialized = doc.to_dict()
if validate_facts(serialized, args.out):
raise SystemExit(
Expand Down
18 changes: 9 additions & 9 deletions harnessing/5-validate/validate-findings/adapters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from pathlib import Path
from typing import ClassVar

from traust.context import load


@dataclass
class Fingerprint:
Expand Down Expand Up @@ -156,7 +158,7 @@ def _classify_content(cls, payload: str | None, cmd: str | None) -> str:

# Step text originates in finding prose — hostile per the threat
# model. Validation and execution of string-form (PoC-derived)
# steps delegate to traust_engine._util.safe_exec under the
# steps delegate to traust-core's safe-exec under the
# "validation-step" profile ($TRAUST_CONFIG_HOME/safe-exec-profiles.yaml):
# substitution/operator denies, per-segment binary allowlist, and
# native pipeline execution with a scrubbed environment — no shell
Expand Down Expand Up @@ -184,23 +186,21 @@ def _classify_content(cls, payload: str | None, cmd: str | None) -> str:
}
)

_safe_exec_mod = None
_safe_exec_policy = None

@classmethod
def _safe_exec(cls):
if cls._safe_exec_mod is None:
import importlib

cls._safe_exec_mod = importlib.import_module("traust_engine._util.safe_exec")
return cls._safe_exec_mod
if cls._safe_exec_policy is None:
cls._safe_exec_policy = load().safe_exec
return cls._safe_exec_policy

@classmethod
def _vet_shell_string(cls, cmd: str) -> tuple[list[str] | None, str]:
"""Vet a PoC-derived command string via safe_exec. Returns
(argv, "") for a pipeless command, (None, "") for an approved
pipeline, or (None, reason) when rejected."""
se = cls._safe_exec()
v = se.vet_command_string(cmd, se.get_profile(cls._SAFE_EXEC_PROFILE))
v = se.vet_command_string(cmd, se.profile(cls._SAFE_EXEC_PROFILE))
if not v.ok:
return None, v.reason
if len(v.segments) == 1:
Expand All @@ -213,7 +213,7 @@ def _run(
) -> tuple[int, str, str]:
if isinstance(cmd, str):
se = cls._safe_exec()
profile = se.get_profile(cls._SAFE_EXEC_PROFILE)
profile = se.profile(cls._SAFE_EXEC_PROFILE)
v = se.vet_command_string(cmd, profile)
if not v.ok:
return 126, "", f"[step blocked: {v.reason}]"
Expand Down
10 changes: 5 additions & 5 deletions harnessing/validation-fuzz-dashboard/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,15 @@ If `summary` was requested, also print the verdict-by-severity table.
| `Live-validation-fuzz-dashboard.html` | Self-contained dashboard: hero (confirmed-exploitable), KPI tiles, verdict-by-severity stacked bars, coverage meter, top-targets bars, fuzz pattern hit-rates, confirmed fuzz-bug and confirmed-Critical tables. |
| `Live-validation-fuzz-dashboard.md` | Markdown one-pager with the same numbers — headline, verdict×severity table, top targets, fuzz hit-rates/bugs, confirmed-Critical list. Paste-ready for docs or Slack. |

Both outputs embed the standard population block (python3 -m traust.cli corpus) for
cross-dashboard reconciliation.
*Retired:* the standard population block. It read the corpus config, which the
engine no longer loads, so it was always omitted.

The script is idempotent and read-only over its inputs — re-run it whenever new
validation reports land or the fuzz summary changes.

## Trends

Each run appends `confirmed_exploitable` / `refuted` / `checks_attempted` /
*Retired:* the run no longer appends `confirmed_exploitable` / `refuted` / `checks_attempted` /
`chains_confirmed` / `fuzz_bugs` (source `validation-fuzz`) to the central
metrics ledger (python3 -m traust.cli metrics history, no-op when unchanged) and injects
a trend-vs-previous banner into both outputs.
metrics ledger or injects a trend-vs-previous banner (the metrics history is retired; see
[`traust-metrics`](../traust-metrics/SKILL.md)).
Loading
Loading