fix(assessment): harden capability evidence trust
This commit is contained in:
@@ -360,7 +360,11 @@ windows. Only `active` and `next` entries are trusted; unknown statuses,
|
||||
duplicate key IDs and malformed structured keyrings fail closed, while revoked,
|
||||
disabled and retired entries are ignored. Do not establish this trust file by
|
||||
downloading it in the same rerun. Public JSON responses are bounded to 16 MiB.
|
||||
Use `--json` or `--output PATH` for automation.
|
||||
Use `--json` or `--output PATH` for automation. Evidence and report files are
|
||||
written through a bounded same-directory atomic replacement, with mode `0600`
|
||||
and file and directory `fsync`. All parent directories must already exist, no
|
||||
parent component may be a symlink, and an existing symlink or non-regular output
|
||||
target is rejected.
|
||||
|
||||
Exit status `0` means the assessed release metadata is current, `2` means
|
||||
explicit review is required, and `1` means the schema or trust checks are
|
||||
@@ -385,7 +389,7 @@ that observation to the assessment and signed catalog:
|
||||
--output /var/tmp/govoplan-fit-review.json
|
||||
```
|
||||
|
||||
The collector follows the strict
|
||||
The collector follows the strict version `0.2.0`
|
||||
[`installed-composition-evidence.schema.json`](installed-composition-evidence.schema.json)
|
||||
contract. It enumerates all installed distributions whose normalized name starts
|
||||
with `govoplan-`, compares the enabled assessed package and module-manifest
|
||||
@@ -395,11 +399,18 @@ entries and evidence-backed conclusions.
|
||||
|
||||
For each distribution, the collector also:
|
||||
|
||||
- verifies every supported SHA-256 entry in installed wheel `RECORD` metadata,
|
||||
- verifies every supported SHA-256 immutable wheel-payload entry in installed
|
||||
`RECORD` metadata,
|
||||
within fixed limits of 256 GovOPlaN distributions, 10,000 files and 512 MiB
|
||||
hashed per distribution, 50,000 files and 2 GiB hashed for one collection,
|
||||
and 64 MiB for any single file;
|
||||
- distinguishes immutable VCS commits and archive hashes from editable installs,
|
||||
- accepts package-owned data-file targets below the installation prefix and
|
||||
declared `console_scripts`, while rejecting other traversal, undeclared
|
||||
scripts, paths outside the prefix and leaf symlinks before opening a file;
|
||||
- reports pip-generated, unhashed `__pycache__/*.pyc` entries separately only
|
||||
when their derived source is a hashed payload entry, including generated
|
||||
files below a confined package-owned runtime-data prefix;
|
||||
- distinguishes coherent PEP 610 Git commits and archive hashes from editable installs,
|
||||
local directories, package-index/unknown origins and malformed metadata;
|
||||
- compares a non-editable VCS commit with the assessment commit and, when one is
|
||||
present, the signed catalog `selected_units` commit;
|
||||
@@ -407,16 +418,29 @@ For each distribution, the collector also:
|
||||
and stable error codes. It never writes direct URLs, paths, hostnames,
|
||||
usernames, exception text or file contents.
|
||||
|
||||
`RECORD` agreement proves that the files agree with their installed metadata; it
|
||||
does not make self-consistent metadata a trusted release origin. Similarly, a
|
||||
version string is not artifact integrity. Editable and directory-backed installs
|
||||
`RECORD` `verified` means complete matching coverage of the immutable wheel
|
||||
payload, not every runtime byte. The proof scope exposes the aggregate count of
|
||||
generated unhashed bytecode; runtime activation remains unchecked. `RECORD`
|
||||
agreement proves that payload files agree with installed metadata, but does not
|
||||
make self-consistent metadata a trusted release origin. Similarly, a version
|
||||
string is not artifact integrity. PEP 610 VCS evidence is accepted only for
|
||||
`vcs: git`, a full 40–64 hexadecimal commit, one mutually exclusive provenance
|
||||
form and a coherent bounded URL shape. Editable and directory-backed installs
|
||||
remain mutable even when their small editable-install `RECORD` is valid. Archive
|
||||
or index artifacts without a hash anchored by the assessed release remain
|
||||
unanchored. These conditions cause review rather than being promoted to
|
||||
immutable installed-release proof.
|
||||
|
||||
Use `--installed-evidence PATH` to review evidence collected in a separate
|
||||
environment. Evidence input and output are bounded to 4 MiB. Collection loads
|
||||
Only evidence produced in-process by `--collect-installed-evidence` is a local
|
||||
observation, and it must be no more than five minutes old (with at most 30
|
||||
seconds of future clock skew) at the selected verification time. Use
|
||||
`--installed-evidence PATH` to compare evidence collected in a separate
|
||||
environment. Imported JSON is unsigned and therefore always carries a blocker:
|
||||
it can identify differences but cannot establish checked or valid installed
|
||||
proof. A future signed collector-attestation format is required to cross that
|
||||
boundary. The proof scope records observation mode, collection/evaluation time,
|
||||
freshness, and whether evaluation used current time or an explicit historical
|
||||
override. Evidence input and output are bounded to 4 MiB. Collection loads
|
||||
the `govoplan.modules` entry-point factories in order to read module IDs and
|
||||
manifest versions; that executes installed GovOPlaN manifest code. Run it only
|
||||
inside the installation being assessed and with the same isolation expected for
|
||||
@@ -453,16 +477,35 @@ the tool's deterministic canonicalization.
|
||||
--trusted-keyring /srv/govoplan/trust/catalog-keyring.json \
|
||||
--installed-evidence /srv/govoplan/evidence/installed.json \
|
||||
--boundary-evidence /srv/govoplan/evidence/target-proof.json \
|
||||
--boundary-authority-keyring /srv/govoplan/trust/proof-authorities.json
|
||||
--boundary-authority-keyring /srv/govoplan/trust/proof-authorities.json \
|
||||
--expected-external-provider-subject provider-production
|
||||
```
|
||||
|
||||
With `--installed-evidence`, this command performs comparison and proof-binding
|
||||
diagnostics only: the imported installed document is unsigned, so neither it nor
|
||||
the boundary claim becomes accepted proof. An accepted boundary review requires
|
||||
a trusted orchestrator to retain the direct in-process observation, obtain the
|
||||
external signature over its digest, and call the review library within the
|
||||
five-minute observation window. A future signed collector-attestation format
|
||||
should make that exchange durable across processes.
|
||||
|
||||
Target-environment and production-approval claims must use the assessment's
|
||||
`deployment_profile.id` as `subject_id`. External-provider claims require the
|
||||
operator to supply a bounded opaque expected subject with
|
||||
`--expected-external-provider-subject`; without it, such a claim remains
|
||||
unchecked and blocks. Expected and observed IDs are retained in proof scope.
|
||||
|
||||
The authority keyring is a governance trust root. Do not download or generate it
|
||||
from the proof bundle being checked, and do not reuse catalog signing authority
|
||||
as implicit production authority. A target operator or approver must validate
|
||||
from the proof bundle being checked. The checker rejects proof-authority public
|
||||
keys reused by either the published or independently trusted catalog keyring,
|
||||
and rejects the same proof public-key material assigned to multiple authority
|
||||
IDs. Authority `not_after` is exclusive. A target operator or approver must validate
|
||||
the referenced drill/result artifacts before signing. The rerun verifies the
|
||||
attestation and its bindings; it does not fetch or reinterpret those artifacts.
|
||||
`--verification-time` exists for reproducible historical review. Live admission
|
||||
must use the actual current time.
|
||||
`--verification-time` exists only for reproducible historical review. Supplying
|
||||
it always marks the machine and human report as a **HISTORICAL override**; callers
|
||||
cannot relabel it as current. Live admission must omit it and use the actual
|
||||
current time.
|
||||
|
||||
No boundary bundle or production authority has been supplied for this current
|
||||
assessment. Target environment, provider and production proof therefore remain
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"format": "uri-reference"
|
||||
},
|
||||
"schema_version": {
|
||||
"const": "0.1.0"
|
||||
"const": "0.2.0"
|
||||
},
|
||||
"evidence_kind": {
|
||||
"const": "govoplan.installed-composition"
|
||||
@@ -125,7 +125,7 @@
|
||||
"required": ["kind", "commit"],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": ["vcs-commit", "editable-vcs"]
|
||||
"const": "vcs-commit"
|
||||
},
|
||||
"commit": {
|
||||
"type": "string",
|
||||
@@ -170,6 +170,7 @@
|
||||
"status",
|
||||
"hashed_file_count",
|
||||
"permitted_unhashed_file_count",
|
||||
"generated_unhashed_file_count",
|
||||
"unverifiable_file_count",
|
||||
"missing_file_count",
|
||||
"mismatched_file_count"
|
||||
@@ -190,6 +191,9 @@
|
||||
"permitted_unhashed_file_count": {
|
||||
"$ref": "#/$defs/count"
|
||||
},
|
||||
"generated_unhashed_file_count": {
|
||||
"$ref": "#/$defs/count"
|
||||
},
|
||||
"unverifiable_file_count": {
|
||||
"$ref": "#/$defs/count"
|
||||
},
|
||||
@@ -199,7 +203,51 @@
|
||||
"mismatched_file_count": {
|
||||
"$ref": "#/$defs/count"
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": { "properties": { "status": { "const": "verified" } } },
|
||||
"then": {
|
||||
"properties": {
|
||||
"hashed_file_count": { "minimum": 1 },
|
||||
"permitted_unhashed_file_count": { "minimum": 1 },
|
||||
"unverifiable_file_count": { "const": 0 },
|
||||
"missing_file_count": { "const": 0 },
|
||||
"mismatched_file_count": { "const": 0 }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": { "properties": { "status": { "const": "partial" } } },
|
||||
"then": {
|
||||
"properties": {
|
||||
"hashed_file_count": { "minimum": 1 },
|
||||
"unverifiable_file_count": { "minimum": 1 },
|
||||
"missing_file_count": { "const": 0 },
|
||||
"mismatched_file_count": { "const": 0 }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": { "properties": { "status": { "const": "mismatch" } } },
|
||||
"then": {
|
||||
"anyOf": [
|
||||
{ "properties": { "missing_file_count": { "minimum": 1 } } },
|
||||
{ "properties": { "mismatched_file_count": { "minimum": 1 } } }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": { "properties": { "status": { "const": "unavailable" } } },
|
||||
"then": {
|
||||
"properties": {
|
||||
"hashed_file_count": { "const": 0 },
|
||||
"missing_file_count": { "const": 0 },
|
||||
"mismatched_file_count": { "const": 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"collection_issue": {
|
||||
"type": "object",
|
||||
|
||||
190
tests/test_capability_fit_atomic_io.py
Normal file
190
tests/test_capability_fit_atomic_io.py
Normal file
@@ -0,0 +1,190 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import stat
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
|
||||
META_ROOT = Path(__file__).resolve().parents[1]
|
||||
ASSESSMENT_TOOLS_ROOT = META_ROOT / "tools" / "assessments"
|
||||
RELEASE_TOOLS_ROOT = META_ROOT / "tools" / "release"
|
||||
for tools_root in (ASSESSMENT_TOOLS_ROOT, RELEASE_TOOLS_ROOT):
|
||||
if str(tools_root) not in sys.path:
|
||||
sys.path.insert(0, str(tools_root))
|
||||
|
||||
from govoplan_assessment.atomic_io import ( # noqa: E402
|
||||
AtomicJsonWriteError,
|
||||
atomic_write_json,
|
||||
)
|
||||
|
||||
|
||||
class CapabilityFitAtomicIOTests(unittest.TestCase):
|
||||
def test_writes_private_json_with_expected_content(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
target = Path(temporary_directory) / "evidence.json"
|
||||
payload = {"z": [1, 2], "message": "Grüße"}
|
||||
created_in: list[Path] = []
|
||||
original_mkstemp = tempfile.mkstemp
|
||||
|
||||
def observed_mkstemp(*args, **kwargs):
|
||||
created_in.append(Path(kwargs["dir"]))
|
||||
return original_mkstemp(*args, **kwargs)
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"govoplan_assessment.atomic_io.tempfile.mkstemp",
|
||||
side_effect=observed_mkstemp,
|
||||
),
|
||||
mock.patch(
|
||||
"govoplan_assessment.atomic_io.os.fsync",
|
||||
wraps=os.fsync,
|
||||
) as fsync,
|
||||
):
|
||||
atomic_write_json(target, payload, max_bytes=1024)
|
||||
|
||||
self.assertEqual(payload, json.loads(target.read_text(encoding="utf-8")))
|
||||
self.assertTrue(target.read_bytes().endswith(b"\n"))
|
||||
self.assertEqual(0o600, stat.S_IMODE(target.stat().st_mode))
|
||||
self.assertEqual([target.parent], created_in)
|
||||
self.assertEqual(2, fsync.call_count)
|
||||
|
||||
def test_replaces_existing_regular_file_and_secures_mode(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
target = Path(temporary_directory) / "report.json"
|
||||
target.write_text('{"old": true}\n', encoding="utf-8")
|
||||
target.chmod(0o644)
|
||||
old_handle = target.open("rb")
|
||||
self.addCleanup(old_handle.close)
|
||||
|
||||
atomic_write_json(target, {"new": True}, max_bytes=1024)
|
||||
|
||||
self.assertEqual(b'{"old": true}\n', old_handle.read())
|
||||
self.assertEqual({"new": True}, json.loads(target.read_text("utf-8")))
|
||||
self.assertEqual(0o600, stat.S_IMODE(target.stat().st_mode))
|
||||
|
||||
def test_size_bound_leaves_existing_target_unchanged(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
directory = Path(temporary_directory)
|
||||
target = directory / "evidence.json"
|
||||
original = b'{"original": true}\n'
|
||||
target.write_bytes(original)
|
||||
|
||||
with self.assertRaisesRegex(AtomicJsonWriteError, "size limit"):
|
||||
atomic_write_json(target, {"large": "x" * 100}, max_bytes=32)
|
||||
|
||||
self.assertEqual(original, target.read_bytes())
|
||||
self.assertEqual([], list(directory.glob(".govoplan-json-*.tmp")))
|
||||
|
||||
@unittest.skipUnless(hasattr(os, "symlink"), "symbolic links are unavailable")
|
||||
def test_rejects_symlink_without_modifying_link_or_referent(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
directory = Path(temporary_directory)
|
||||
referent = directory / "outside.json"
|
||||
referent.write_bytes(b'{"keep": true}\n')
|
||||
target = directory / "evidence.json"
|
||||
target.symlink_to(referent.name)
|
||||
|
||||
with self.assertRaisesRegex(AtomicJsonWriteError, "symbolic link"):
|
||||
atomic_write_json(target, {"replace": True}, max_bytes=1024)
|
||||
|
||||
self.assertTrue(target.is_symlink())
|
||||
self.assertEqual(b'{"keep": true}\n', referent.read_bytes())
|
||||
self.assertEqual([], list(directory.glob(".govoplan-json-*.tmp")))
|
||||
|
||||
@unittest.skipUnless(hasattr(os, "symlink"), "symbolic links are unavailable")
|
||||
def test_rejects_symlinked_parent_component(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
directory = Path(temporary_directory)
|
||||
real_parent = directory / "real-parent"
|
||||
real_parent.mkdir()
|
||||
linked_parent = directory / "linked-parent"
|
||||
linked_parent.symlink_to(real_parent, target_is_directory=True)
|
||||
target = linked_parent / "new" / "evidence.json"
|
||||
|
||||
with self.assertRaisesRegex(AtomicJsonWriteError, "parent path"):
|
||||
atomic_write_json(target, {"unsafe": True}, max_bytes=1024)
|
||||
|
||||
self.assertFalse((real_parent / "new").exists())
|
||||
|
||||
def test_requires_existing_parent_without_creating_directories(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
directory = Path(temporary_directory)
|
||||
missing_parent = directory / "missing" / "nested"
|
||||
target = missing_parent / "evidence.json"
|
||||
|
||||
with self.assertRaisesRegex(AtomicJsonWriteError, "already exist"):
|
||||
atomic_write_json(target, {"safe": True}, max_bytes=1024)
|
||||
|
||||
self.assertFalse(missing_parent.exists())
|
||||
|
||||
def test_rejects_post_replace_mode_or_identity_change(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
directory = Path(temporary_directory)
|
||||
target = directory / "evidence.json"
|
||||
original_replace = os.replace
|
||||
|
||||
def insecure_replace(source, destination):
|
||||
original_replace(source, destination)
|
||||
Path(destination).chmod(0o644)
|
||||
|
||||
with mock.patch(
|
||||
"govoplan_assessment.atomic_io.os.replace",
|
||||
side_effect=insecure_replace,
|
||||
):
|
||||
with self.assertRaisesRegex(
|
||||
AtomicJsonWriteError,
|
||||
"permissions changed and were restored",
|
||||
):
|
||||
atomic_write_json(target, {"safe": True}, max_bytes=1024)
|
||||
|
||||
self.assertEqual(0o600, stat.S_IMODE(target.stat().st_mode))
|
||||
self.assertEqual({"safe": True}, json.loads(target.read_text("utf-8")))
|
||||
|
||||
def test_does_not_unlink_unknown_post_replace_inode(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
directory = Path(temporary_directory)
|
||||
target = directory / "evidence.json"
|
||||
original_replace = os.replace
|
||||
|
||||
def replaced_again(source, destination):
|
||||
original_replace(source, destination)
|
||||
Path(destination).unlink()
|
||||
Path(destination).write_bytes(b"unknown replacement\n")
|
||||
|
||||
with mock.patch(
|
||||
"govoplan_assessment.atomic_io.os.replace",
|
||||
side_effect=replaced_again,
|
||||
):
|
||||
with self.assertRaisesRegex(
|
||||
AtomicJsonWriteError,
|
||||
"did not preserve",
|
||||
):
|
||||
atomic_write_json(target, {"secret": True}, max_bytes=1024)
|
||||
|
||||
self.assertEqual(b"unknown replacement\n", target.read_bytes())
|
||||
|
||||
def test_replace_failure_removes_private_temporary_file(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
directory = Path(temporary_directory)
|
||||
target = directory / "evidence.json"
|
||||
|
||||
with mock.patch(
|
||||
"govoplan_assessment.atomic_io.os.replace",
|
||||
side_effect=OSError("simulated replacement failure"),
|
||||
):
|
||||
with self.assertRaisesRegex(
|
||||
AtomicJsonWriteError, "could not be written atomically"
|
||||
):
|
||||
atomic_write_json(target, {"safe": True}, max_bytes=1024)
|
||||
|
||||
self.assertFalse(target.exists())
|
||||
self.assertEqual([], list(directory.glob(".govoplan-json-*.tmp")))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -7,6 +7,7 @@ import hashlib
|
||||
from importlib import metadata
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
@@ -24,8 +25,12 @@ for tools_root in (ASSESSMENT_TOOLS_ROOT, RELEASE_TOOLS_ROOT):
|
||||
if str(tools_root) not in sys.path:
|
||||
sys.path.insert(0, str(tools_root))
|
||||
|
||||
from govoplan_assessment.capability_fit import review_capability_fit # noqa: E402
|
||||
from govoplan_assessment.capability_fit import ( # noqa: E402
|
||||
render_review,
|
||||
review_capability_fit,
|
||||
)
|
||||
from govoplan_assessment.evidence import ( # noqa: E402
|
||||
_bounded_file_sha256,
|
||||
canonical_bytes,
|
||||
canonical_sha256,
|
||||
collect_installed_composition,
|
||||
@@ -205,6 +210,8 @@ class CapabilityFitEvidenceTests(unittest.TestCase):
|
||||
selected_commits={},
|
||||
evidence=evidence,
|
||||
schema=self.installed_schema,
|
||||
observation_mode="direct_local",
|
||||
verification_time=datetime(2026, 7, 23, 12, tzinfo=UTC),
|
||||
)
|
||||
|
||||
codes = {item.code for item in result.findings}
|
||||
@@ -213,6 +220,86 @@ class CapabilityFitEvidenceTests(unittest.TestCase):
|
||||
self.assertTrue(result.proof_scope["installed_artifacts"]["checked"])
|
||||
self.assertFalse(result.proof_scope["installed_artifacts"]["valid"])
|
||||
|
||||
def test_imported_unsigned_installed_evidence_is_comparison_only(self) -> None:
|
||||
catalog, keyring = signed_catalog(self.assessment)
|
||||
evidence = matching_installed_evidence(self.assessment)
|
||||
|
||||
report = self.review(
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
installed_evidence=evidence,
|
||||
installed_evidence_mode="imported_unsigned",
|
||||
)
|
||||
|
||||
installed = report["proof_scope"]["installed_artifacts"]
|
||||
self.assertEqual("blocked", report["status"])
|
||||
self.assertFalse(installed["checked"])
|
||||
self.assertIsNone(installed["valid"])
|
||||
self.assertTrue(installed["comparison_valid"])
|
||||
self.assertEqual(
|
||||
"unsigned_import",
|
||||
report["proof_scope"]["installed_evidence_observation"]["freshness"],
|
||||
)
|
||||
self.assertIn(
|
||||
"installed_evidence_unsigned_import",
|
||||
{item["code"] for item in report["findings"]},
|
||||
)
|
||||
|
||||
def test_direct_installed_evidence_must_be_fresh_and_not_from_future(self) -> None:
|
||||
catalog, keyring = signed_catalog(self.assessment)
|
||||
cases = (
|
||||
("2026-07-23T11:54:59Z", "installed_evidence_stale", "stale"),
|
||||
("2026-07-23T12:00:31Z", "installed_evidence_from_future", "future"),
|
||||
)
|
||||
for collected_at, expected_code, expected_freshness in cases:
|
||||
with self.subTest(collected_at=collected_at):
|
||||
evidence = matching_installed_evidence(self.assessment)
|
||||
evidence["collected_at"] = collected_at
|
||||
report = self.review(
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
installed_evidence=evidence,
|
||||
)
|
||||
|
||||
self.assertEqual("blocked", report["status"])
|
||||
self.assertFalse(
|
||||
report["proof_scope"]["installed_artifacts"]["checked"]
|
||||
)
|
||||
self.assertIn(
|
||||
expected_code,
|
||||
{item["code"] for item in report["findings"]},
|
||||
)
|
||||
self.assertEqual(
|
||||
expected_freshness,
|
||||
report["proof_scope"]["installed_evidence_observation"][
|
||||
"freshness"
|
||||
],
|
||||
)
|
||||
|
||||
def test_forged_verified_record_counters_fail_semantic_validation(self) -> None:
|
||||
catalog, keyring = signed_catalog(self.assessment)
|
||||
evidence = matching_installed_evidence(self.assessment)
|
||||
evidence["artifacts"][0]["record_integrity"].update(
|
||||
hashed_file_count=0,
|
||||
mismatched_file_count=1,
|
||||
)
|
||||
relaxed_schema = deepcopy(self.installed_schema)
|
||||
relaxed_schema["$defs"]["record_integrity"].pop("allOf")
|
||||
|
||||
report = self.review(
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
installed_evidence=evidence,
|
||||
installed_schema=relaxed_schema,
|
||||
)
|
||||
|
||||
self.assertEqual("review_required", report["status"])
|
||||
self.assertIn(
|
||||
"installed_record_integrity_invalid",
|
||||
{item["code"] for item in report["findings"]},
|
||||
)
|
||||
self.assertFalse(report["proof_scope"]["installed_record_integrity"]["valid"])
|
||||
|
||||
def test_collector_verifies_record_and_redacts_direct_url(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
@@ -251,6 +338,225 @@ class CapabilityFitEvidenceTests(unittest.TestCase):
|
||||
tampered["artifacts"][0]["record_integrity"]["mismatched_file_count"],
|
||||
)
|
||||
|
||||
def test_collector_accepts_real_core_wheel_payload_scripts_data_and_pyc(
|
||||
self,
|
||||
) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
wheels = root / "wheels"
|
||||
prefix = root / "prefix"
|
||||
wheels.mkdir()
|
||||
build_python = (
|
||||
Path(sys.base_prefix)
|
||||
/ "bin"
|
||||
/ (f"python{sys.version_info.major}.{sys.version_info.minor}")
|
||||
)
|
||||
subprocess.run(
|
||||
(
|
||||
str(build_python),
|
||||
"-m",
|
||||
"pip",
|
||||
"wheel",
|
||||
"--no-deps",
|
||||
"--no-build-isolation",
|
||||
"--wheel-dir",
|
||||
str(wheels),
|
||||
str(META_ROOT.parent / "govoplan-core"),
|
||||
),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
wheel = next(wheels.glob("govoplan_core-*.whl"))
|
||||
subprocess.run(
|
||||
(
|
||||
str(build_python),
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--no-deps",
|
||||
"--ignore-installed",
|
||||
"--prefix",
|
||||
str(prefix),
|
||||
str(wheel),
|
||||
),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
site_packages = next(prefix.glob("lib/python*/site-packages"))
|
||||
dist_info = next(site_packages.glob("govoplan_core-*.dist-info"))
|
||||
distribution = metadata.PathDistribution(dist_info)
|
||||
record_paths = {str(item) for item in distribution.files or ()}
|
||||
self.assertTrue(
|
||||
any(
|
||||
path.startswith("../../../govoplan_core_runtime/")
|
||||
for path in record_paths
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
any(path.startswith("../../../bin/govoplan-") for path in record_paths)
|
||||
)
|
||||
with (
|
||||
mock.patch.object(sys, "path", [str(site_packages), *sys.path]),
|
||||
mock.patch.object(sys, "prefix", str(prefix)),
|
||||
):
|
||||
evidence = collect_installed_composition(
|
||||
assessment=self.assessment,
|
||||
collected_at=datetime(2026, 7, 23, 11, 58, tzinfo=UTC),
|
||||
distributions=[distribution],
|
||||
)
|
||||
|
||||
record = evidence["artifacts"][0]["record_integrity"]
|
||||
self.assertEqual("verified", record["status"])
|
||||
self.assertGreater(record["hashed_file_count"], 0)
|
||||
self.assertGreater(record["permitted_unhashed_file_count"], 0)
|
||||
self.assertGreater(record["generated_unhashed_file_count"], 0)
|
||||
self.assertEqual(0, record["unverifiable_file_count"])
|
||||
self.assertEqual(
|
||||
(), validate_payload(payload=evidence, schema=self.installed_schema)
|
||||
)
|
||||
|
||||
def test_record_traversal_and_leaf_symlinks_are_never_hashed(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
prefix = Path(temp_dir) / "prefix"
|
||||
site_packages = prefix / "lib" / "python-test" / "site-packages"
|
||||
site_packages.mkdir(parents=True)
|
||||
traversal = create_traversal_distribution(prefix, site_packages)
|
||||
normal, payload = create_distribution(site_packages)
|
||||
outside = Path(temp_dir) / "outside.py"
|
||||
outside.write_text(payload.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
payload.unlink()
|
||||
payload.symlink_to(outside)
|
||||
with (
|
||||
mock.patch.object(sys, "path", [str(site_packages), *sys.path]),
|
||||
mock.patch.object(sys, "prefix", str(prefix)),
|
||||
):
|
||||
evidence = collect_installed_composition(
|
||||
assessment=self.assessment,
|
||||
collected_at=datetime(2026, 7, 23, 11, 58, tzinfo=UTC),
|
||||
distributions=[traversal, normal],
|
||||
)
|
||||
|
||||
records = {
|
||||
item["package_name"]: item["record_integrity"]
|
||||
for item in evidence["artifacts"]
|
||||
}
|
||||
self.assertEqual("partial", records["govoplan-traversal"]["status"])
|
||||
self.assertGreaterEqual(
|
||||
records["govoplan-traversal"]["unverifiable_file_count"], 2
|
||||
)
|
||||
self.assertEqual("partial", records["govoplan-demo"]["status"])
|
||||
self.assertGreater(records["govoplan-demo"]["unverifiable_file_count"], 0)
|
||||
|
||||
def test_bounded_hash_stops_at_opened_file_snapshot(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
path = Path(temp_dir) / "growing.bin"
|
||||
path.write_bytes(b"0123456789")
|
||||
actual_stat = path.stat()
|
||||
initial_stat = SimpleNamespace(st_mode=actual_stat.st_mode, st_size=4)
|
||||
final_stat = SimpleNamespace(st_mode=actual_stat.st_mode, st_size=10)
|
||||
with mock.patch(
|
||||
"govoplan_assessment.evidence.os.fstat",
|
||||
side_effect=(initial_stat, final_stat),
|
||||
):
|
||||
_, hashed_bytes, exceeded = _bounded_file_sha256(path, max_bytes=8)
|
||||
|
||||
self.assertEqual(4, hashed_bytes)
|
||||
self.assertTrue(exceeded)
|
||||
|
||||
def test_collector_rejects_unbounded_or_secret_shaped_metadata(self) -> None:
|
||||
distributions = [
|
||||
FakeDistribution(
|
||||
name="govoplan-secret-user@private-host/path",
|
||||
entry_points=[],
|
||||
),
|
||||
FakeDistribution(
|
||||
version="1.0.0/private-user@host",
|
||||
entry_points=[],
|
||||
),
|
||||
FakeDistribution(
|
||||
entry_points=[
|
||||
FakeEntryPoint(
|
||||
name="safe-module",
|
||||
manifest_id="secret-user@private-host/path",
|
||||
)
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
evidence = collect_installed_composition(
|
||||
assessment=self.assessment,
|
||||
collected_at=datetime(2026, 7, 23, 11, 58, tzinfo=UTC),
|
||||
distributions=distributions,
|
||||
)
|
||||
|
||||
encoded = json.dumps(evidence, sort_keys=True)
|
||||
self.assertNotIn("secret-user", encoded)
|
||||
self.assertNotIn("private-host", encoded)
|
||||
self.assertNotIn("private-user", encoded)
|
||||
self.assertTrue(
|
||||
any(
|
||||
item["code"]
|
||||
in {
|
||||
"distribution-metadata-invalid",
|
||||
"module-entry-point-invalid",
|
||||
}
|
||||
for item in evidence["collection_issues"]
|
||||
)
|
||||
)
|
||||
|
||||
def test_pep610_git_provenance_requires_one_coherent_git_form(self) -> None:
|
||||
valid_commit = "a" * 40
|
||||
cases = (
|
||||
(
|
||||
"valid",
|
||||
{
|
||||
"url": "https://example.invalid/govoplan-many.git",
|
||||
"vcs_info": {"vcs": "git", "commit_id": valid_commit},
|
||||
},
|
||||
"vcs-commit",
|
||||
),
|
||||
(
|
||||
"svn",
|
||||
{
|
||||
"url": "https://example.invalid/govoplan-many.git",
|
||||
"vcs_info": {"vcs": "svn", "commit_id": valid_commit},
|
||||
},
|
||||
"malformed-direct-url",
|
||||
),
|
||||
(
|
||||
"missing-vcs",
|
||||
{
|
||||
"url": "https://example.invalid/govoplan-many.git",
|
||||
"vcs_info": {"commit_id": valid_commit},
|
||||
},
|
||||
"malformed-direct-url",
|
||||
),
|
||||
(
|
||||
"ambiguous",
|
||||
{
|
||||
"url": "file:///govoplan-many",
|
||||
"vcs_info": {"vcs": "git", "commit_id": valid_commit},
|
||||
"dir_info": {"editable": True},
|
||||
},
|
||||
"malformed-direct-url",
|
||||
),
|
||||
)
|
||||
for label, direct_url, expected_kind in cases:
|
||||
with self.subTest(label=label):
|
||||
evidence = collect_installed_composition(
|
||||
assessment=self.assessment,
|
||||
collected_at=datetime(2026, 7, 23, 11, 58, tzinfo=UTC),
|
||||
distributions=[
|
||||
FakeDistribution(entry_points=[], direct_url=direct_url)
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
expected_kind,
|
||||
evidence["artifacts"][0]["source_provenance"]["kind"],
|
||||
)
|
||||
|
||||
def test_entry_point_limit_is_explicit_instead_of_silent_truncation(self) -> None:
|
||||
distribution = FakeDistribution(
|
||||
entry_points=[FakeEntryPoint(name=f"module-{index}") for index in range(17)]
|
||||
@@ -320,6 +626,248 @@ class CapabilityFitEvidenceTests(unittest.TestCase):
|
||||
self.assertTrue(report["proof_scope"]["target_environment"]["valid"])
|
||||
self.assertFalse(report["proof_scope"]["external_providers"]["checked"])
|
||||
self.assertFalse(report["proof_scope"]["production_approval"]["checked"])
|
||||
self.assertEqual(
|
||||
self.assessment["deployment_profile"]["id"],
|
||||
report["proof_scope"]["target_environment"]["expected_subject_id"],
|
||||
)
|
||||
self.assertEqual(
|
||||
self.assessment["deployment_profile"]["id"],
|
||||
report["proof_scope"]["target_environment"]["observed_subject_id"],
|
||||
)
|
||||
|
||||
def test_external_provider_claim_requires_explicit_matching_subject(self) -> None:
|
||||
catalog, keyring = signed_catalog(self.assessment)
|
||||
installed = matching_installed_evidence(self.assessment)
|
||||
proof, authority = signed_boundary_evidence(
|
||||
assessment=self.assessment,
|
||||
installed=installed,
|
||||
claims=[boundary_claim("external_providers", "passed")],
|
||||
allowed_scopes=["external_providers"],
|
||||
)
|
||||
|
||||
unconfigured = self.review(
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
installed_evidence=installed,
|
||||
boundary_evidence=proof,
|
||||
authority_keyring=authority,
|
||||
)
|
||||
accepted = self.review(
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
installed_evidence=installed,
|
||||
boundary_evidence=proof,
|
||||
authority_keyring=authority,
|
||||
expected_external_provider_subject="provider:test",
|
||||
)
|
||||
|
||||
self.assertEqual("blocked", unconfigured["status"])
|
||||
self.assertIn(
|
||||
"boundary_provider_subject_unconfigured",
|
||||
{item["code"] for item in unconfigured["findings"]},
|
||||
)
|
||||
self.assertEqual("current", accepted["status"])
|
||||
provider_scope = accepted["proof_scope"]["external_providers"]
|
||||
self.assertEqual("provider:test", provider_scope["expected_subject_id"])
|
||||
self.assertEqual("provider:test", provider_scope["observed_subject_id"])
|
||||
|
||||
def test_boundary_subject_mismatch_blocks_without_echoing_invalid_config(
|
||||
self,
|
||||
) -> None:
|
||||
catalog, keyring = signed_catalog(self.assessment)
|
||||
installed = matching_installed_evidence(self.assessment)
|
||||
proof, authority = signed_boundary_evidence(
|
||||
assessment=self.assessment,
|
||||
installed=installed,
|
||||
claims=[
|
||||
boundary_claim(
|
||||
"target_environment",
|
||||
"passed",
|
||||
subject_id="another-deployment",
|
||||
)
|
||||
],
|
||||
allowed_scopes=["target_environment"],
|
||||
)
|
||||
mismatch = self.review(
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
installed_evidence=installed,
|
||||
boundary_evidence=proof,
|
||||
authority_keyring=authority,
|
||||
)
|
||||
invalid_config = self.review(
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
installed_evidence=installed,
|
||||
expected_external_provider_subject="https://private.invalid/secret",
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"boundary_claim_subject_mismatch",
|
||||
{item["code"] for item in mismatch["findings"]},
|
||||
)
|
||||
encoded = json.dumps(invalid_config, sort_keys=True)
|
||||
self.assertIn("boundary_provider_subject_invalid", encoded)
|
||||
self.assertNotIn("private.invalid", encoded)
|
||||
self.assertNotIn("secret", encoded)
|
||||
|
||||
def test_authorized_negative_claim_creates_scope_specific_review_target(
|
||||
self,
|
||||
) -> None:
|
||||
catalog, keyring = signed_catalog(self.assessment)
|
||||
installed = matching_installed_evidence(self.assessment)
|
||||
proof, authority = signed_boundary_evidence(
|
||||
assessment=self.assessment,
|
||||
installed=installed,
|
||||
claims=[
|
||||
boundary_claim("target_environment", "failed"),
|
||||
boundary_claim("production_approval", "rejected"),
|
||||
],
|
||||
allowed_scopes=["target_environment", "production_approval"],
|
||||
)
|
||||
|
||||
report = self.review(
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
installed_evidence=installed,
|
||||
boundary_evidence=proof,
|
||||
authority_keyring=authority,
|
||||
)
|
||||
|
||||
targets = {item["id"] for item in report["review_targets"]}
|
||||
self.assertIn(
|
||||
"boundary.target_environment.production-like-dev",
|
||||
targets,
|
||||
)
|
||||
self.assertIn(
|
||||
"boundary.production_approval.production-like-dev",
|
||||
targets,
|
||||
)
|
||||
|
||||
def test_proof_authorities_must_be_independent_and_key_ids_unambiguous(
|
||||
self,
|
||||
) -> None:
|
||||
catalog, keyring = signed_catalog(self.assessment)
|
||||
installed = matching_installed_evidence(self.assessment)
|
||||
proof, authority = signed_boundary_evidence(
|
||||
assessment=self.assessment,
|
||||
installed=installed,
|
||||
claims=[boundary_claim("target_environment", "passed")],
|
||||
allowed_scopes=["target_environment"],
|
||||
)
|
||||
release_public_key = keyring["keys"][0]["public_key"]
|
||||
reused = deepcopy(authority)
|
||||
reused["keys"][0]["public_key"] = release_public_key
|
||||
duplicate = deepcopy(authority)
|
||||
duplicate["keys"].append(
|
||||
{
|
||||
**deepcopy(duplicate["keys"][0]),
|
||||
"key_id": "authority:duplicate-material",
|
||||
"allowed_scopes": ["production_approval"],
|
||||
}
|
||||
)
|
||||
|
||||
reused_report = self.review(
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
installed_evidence=installed,
|
||||
boundary_evidence=proof,
|
||||
authority_keyring=reused,
|
||||
)
|
||||
duplicate_report = self.review(
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
installed_evidence=installed,
|
||||
boundary_evidence=proof,
|
||||
authority_keyring=duplicate,
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"boundary_authority_reuses_release_key",
|
||||
{item["code"] for item in reused_report["findings"]},
|
||||
)
|
||||
self.assertIn(
|
||||
"boundary_authority_public_key_duplicate",
|
||||
{item["code"] for item in duplicate_report["findings"]},
|
||||
)
|
||||
self.assertFalse(
|
||||
duplicate_report["proof_scope"]["target_environment"]["checked"]
|
||||
)
|
||||
|
||||
def test_authority_not_after_is_exclusive(self) -> None:
|
||||
catalog, keyring = signed_catalog(self.assessment)
|
||||
installed = matching_installed_evidence(self.assessment)
|
||||
proof, authority = signed_boundary_evidence(
|
||||
assessment=self.assessment,
|
||||
installed=installed,
|
||||
claims=[boundary_claim("target_environment", "passed")],
|
||||
allowed_scopes=["target_environment"],
|
||||
)
|
||||
authority["keys"][0]["not_after"] = "2026-07-23T12:00:00Z"
|
||||
|
||||
report = self.review(
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
installed_evidence=installed,
|
||||
boundary_evidence=proof,
|
||||
authority_keyring=authority,
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"boundary_authority_untrusted",
|
||||
{item["code"] for item in report["findings"]},
|
||||
)
|
||||
|
||||
def test_historical_verification_and_generated_runtime_gap_are_visible(
|
||||
self,
|
||||
) -> None:
|
||||
catalog, keyring = signed_catalog(self.assessment)
|
||||
evidence = matching_installed_evidence(self.assessment)
|
||||
evidence["artifacts"][0]["record_integrity"][
|
||||
"generated_unhashed_file_count"
|
||||
] = 3
|
||||
|
||||
report = self.review(
|
||||
catalog=catalog,
|
||||
keyring=keyring,
|
||||
installed_evidence=evidence,
|
||||
)
|
||||
|
||||
observation = report["proof_scope"]["installed_evidence_observation"]
|
||||
self.assertEqual("historical_override", observation["verification_mode"])
|
||||
self.assertEqual(
|
||||
3,
|
||||
report["proof_scope"]["installed_record_integrity"][
|
||||
"generated_unhashed_file_count"
|
||||
],
|
||||
)
|
||||
self.assertIn("HISTORICAL override", render_review(report))
|
||||
|
||||
def test_invalid_internal_verification_mode_is_bounded_and_blocking(self) -> None:
|
||||
evidence = matching_installed_evidence(self.assessment)
|
||||
result = review_installed_composition(
|
||||
assessment=self.assessment,
|
||||
catalog_entries={},
|
||||
selected_versions={},
|
||||
selected_commits={},
|
||||
evidence=evidence,
|
||||
schema=self.installed_schema,
|
||||
observation_mode="direct_local",
|
||||
verification_time=datetime(2026, 7, 23, 12, tzinfo=UTC),
|
||||
verification_mode="https://private.invalid/secret",
|
||||
)
|
||||
|
||||
encoded = json.dumps(result.proof_scope, sort_keys=True)
|
||||
self.assertIn(
|
||||
"installed_evidence_verification_mode",
|
||||
{item.code for item in result.findings},
|
||||
)
|
||||
self.assertEqual(
|
||||
"invalid",
|
||||
result.proof_scope["installed_evidence_observation"]["verification_mode"],
|
||||
)
|
||||
self.assertFalse(result.proof_scope["installed_artifacts"]["checked"])
|
||||
self.assertNotIn("private.invalid", encoded)
|
||||
|
||||
def test_unauthorized_production_claim_keeps_every_claim_unchecked(self) -> None:
|
||||
catalog, keyring = signed_catalog(self.assessment)
|
||||
@@ -401,6 +949,10 @@ class CapabilityFitEvidenceTests(unittest.TestCase):
|
||||
installed_evidence: dict[str, object] | None = None,
|
||||
boundary_evidence: dict[str, object] | None = None,
|
||||
authority_keyring: dict[str, object] | None = None,
|
||||
installed_evidence_mode: str = "direct_local",
|
||||
installed_schema: dict[str, object] | None = None,
|
||||
expected_external_provider_subject: str | None = None,
|
||||
verification_time: datetime | None = datetime(2026, 7, 23, 12, tzinfo=UTC),
|
||||
) -> dict[str, object]:
|
||||
return review_capability_fit(
|
||||
assessment=deepcopy(self.assessment),
|
||||
@@ -409,7 +961,7 @@ class CapabilityFitEvidenceTests(unittest.TestCase):
|
||||
published_keyring=keyring,
|
||||
trusted_keyring=keyring,
|
||||
installed_evidence=installed_evidence,
|
||||
installed_evidence_schema=self.installed_schema
|
||||
installed_evidence_schema=(installed_schema or self.installed_schema)
|
||||
if installed_evidence is not None
|
||||
else None,
|
||||
boundary_evidence=boundary_evidence,
|
||||
@@ -420,18 +972,20 @@ class CapabilityFitEvidenceTests(unittest.TestCase):
|
||||
boundary_authority_keyring_schema=self.authority_schema
|
||||
if boundary_evidence is not None
|
||||
else None,
|
||||
verification_time=datetime(2026, 7, 23, 12, tzinfo=UTC),
|
||||
verification_time=verification_time,
|
||||
installed_evidence_mode=installed_evidence_mode,
|
||||
expected_external_provider_subject=expected_external_provider_subject,
|
||||
)
|
||||
|
||||
|
||||
def matching_installed_evidence(assessment: dict[str, object]) -> dict[str, object]:
|
||||
return {
|
||||
"$schema": "./installed-composition-evidence.schema.json",
|
||||
"schema_version": "0.1.0",
|
||||
"schema_version": "0.2.0",
|
||||
"evidence_kind": "govoplan.installed-composition",
|
||||
"assessment_id": assessment["assessment_id"],
|
||||
"assessment_release": assessment["release"]["ref"],
|
||||
"collected_at": "2026-07-22T12:00:00Z",
|
||||
"collected_at": "2026-07-23T11:58:00Z",
|
||||
"scope": "current-python-environment.govoplan-distributions",
|
||||
"artifacts": [
|
||||
artifact(
|
||||
@@ -461,6 +1015,7 @@ def artifact(
|
||||
"status": "verified",
|
||||
"hashed_file_count": 1,
|
||||
"permitted_unhashed_file_count": 1,
|
||||
"generated_unhashed_file_count": 0,
|
||||
"unverifiable_file_count": 0,
|
||||
"missing_file_count": 0,
|
||||
"mismatched_file_count": 0,
|
||||
@@ -468,11 +1023,19 @@ def artifact(
|
||||
}
|
||||
|
||||
|
||||
def boundary_claim(scope: str, result: str) -> dict[str, object]:
|
||||
def boundary_claim(
|
||||
scope: str,
|
||||
result: str,
|
||||
*,
|
||||
subject_id: str | None = None,
|
||||
) -> dict[str, object]:
|
||||
default_subject = (
|
||||
"provider:test" if scope == "external_providers" else "production-like-dev"
|
||||
)
|
||||
return {
|
||||
"scope": scope,
|
||||
"result": result,
|
||||
"subject_id": f"subject:{scope}",
|
||||
"subject_id": subject_id or default_subject,
|
||||
"control_ids": [f"control:{scope}"],
|
||||
"artifacts": [
|
||||
{
|
||||
@@ -579,19 +1142,72 @@ def create_distribution(
|
||||
return metadata.PathDistribution(dist_info), payload_file
|
||||
|
||||
|
||||
def create_traversal_distribution(
|
||||
prefix: Path,
|
||||
site_packages: Path,
|
||||
) -> metadata.PathDistribution:
|
||||
dist_info = site_packages / "govoplan_traversal-1.0.0.dist-info"
|
||||
dist_info.mkdir()
|
||||
metadata_file = dist_info / "METADATA"
|
||||
metadata_file.write_text(
|
||||
"Metadata-Version: 2.1\nName: govoplan-traversal\nVersion: 1.0.0\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
unowned_file = prefix / "unowned_runtime" / "private.py"
|
||||
unowned_file.parent.mkdir()
|
||||
unowned_file.write_text("private = True\n", encoding="utf-8")
|
||||
undeclared_script = prefix / "bin" / "govoplan-not-declared"
|
||||
undeclared_script.parent.mkdir()
|
||||
undeclared_script.write_text("#!/bin/sh\n", encoding="utf-8")
|
||||
metadata_path = metadata_file.relative_to(site_packages).as_posix()
|
||||
unowned_path = "../../../unowned_runtime/private.py"
|
||||
record_path = (dist_info / "RECORD").relative_to(site_packages).as_posix()
|
||||
(dist_info / "RECORD").write_text(
|
||||
"\n".join(
|
||||
(
|
||||
record_row(metadata_path, metadata_file.read_bytes()),
|
||||
record_row(unowned_path, unowned_file.read_bytes()),
|
||||
record_row(
|
||||
"../../../bin/govoplan-not-declared",
|
||||
undeclared_script.read_bytes(),
|
||||
),
|
||||
f"{record_path},,",
|
||||
)
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return metadata.PathDistribution(dist_info)
|
||||
|
||||
|
||||
def record_row(path: str, content: bytes) -> str:
|
||||
digest = base64.urlsafe_b64encode(hashlib.sha256(content).digest()).decode("ascii")
|
||||
return f"{path},sha256={digest.rstrip('=')},{len(content)}"
|
||||
|
||||
|
||||
class FakeEntryPoint:
|
||||
group = "govoplan.modules"
|
||||
value = "fixture:get_manifest"
|
||||
|
||||
def __init__(self, *, name: str) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
manifest_id: str | None = None,
|
||||
manifest_version: str = "1.0.0",
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.manifest_id = manifest_id or name
|
||||
self.manifest_version = manifest_version
|
||||
|
||||
def load(self):
|
||||
return lambda: SimpleNamespace(id=self.name, version="1.0.0")
|
||||
return lambda: SimpleNamespace(
|
||||
id=self.manifest_id,
|
||||
version=self.manifest_version,
|
||||
)
|
||||
|
||||
|
||||
class FakeDistribution:
|
||||
version = "1.0.0"
|
||||
files = ()
|
||||
|
||||
def __init__(
|
||||
@@ -599,8 +1215,11 @@ class FakeDistribution:
|
||||
*,
|
||||
entry_points: list[FakeEntryPoint],
|
||||
direct_url: dict[str, object] | None = None,
|
||||
name: str = "govoplan-many",
|
||||
version: str = "1.0.0",
|
||||
) -> None:
|
||||
self.metadata = {"Name": "govoplan-many"}
|
||||
self.metadata = {"Name": name}
|
||||
self.version = version
|
||||
self.entry_points = entry_points
|
||||
self.direct_url = direct_url
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
@@ -22,6 +23,10 @@ from govoplan_assessment import ( # noqa: E402
|
||||
render_review,
|
||||
review_capability_fit,
|
||||
)
|
||||
from govoplan_assessment.atomic_io import ( # noqa: E402
|
||||
AtomicJsonWriteError,
|
||||
atomic_write_json,
|
||||
)
|
||||
from govoplan_assessment.evidence import validate_payload # noqa: E402
|
||||
from govoplan_release.catalog import DEFAULT_PUBLIC_BASE_URL, fetch_json # noqa: E402
|
||||
|
||||
@@ -29,6 +34,8 @@ from govoplan_release.catalog import DEFAULT_PUBLIC_BASE_URL, fetch_json # noqa
|
||||
TRUSTED_KEYRING_FILE_ENV = "GOVOPLAN_MODULE_PACKAGE_CATALOG_TRUSTED_KEYS_FILE"
|
||||
MAX_ASSESSMENT_INPUT_BYTES = 16 * 1024 * 1024
|
||||
MAX_EVIDENCE_INPUT_BYTES = 4 * 1024 * 1024
|
||||
MAX_REPORT_OUTPUT_BYTES = 16 * 1024 * 1024
|
||||
OPAQUE_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$")
|
||||
|
||||
|
||||
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
@@ -130,6 +137,13 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
"--verification-time",
|
||||
help="Optional ISO-8601 time for reproducible boundary-proof verification.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--expected-external-provider-subject",
|
||||
help=(
|
||||
"Opaque provider-environment ID that external-provider proof must match; "
|
||||
"never supply a URL, credential, or endpoint identifier."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json", action="store_true", help="Print the machine-readable review report."
|
||||
)
|
||||
@@ -157,6 +171,13 @@ def main(argv: list[str] | None = None) -> int:
|
||||
raise SystemExit(
|
||||
"--boundary-evidence requires --installed-evidence or --collect-installed-evidence"
|
||||
)
|
||||
if (
|
||||
args.expected_external_provider_subject is not None
|
||||
and OPAQUE_ID_PATTERN.fullmatch(args.expected_external_provider_subject) is None
|
||||
):
|
||||
raise SystemExit(
|
||||
"--expected-external-provider-subject must be a bounded opaque ID"
|
||||
)
|
||||
if (
|
||||
args.collect_installed_evidence is not None
|
||||
and args.output is not None
|
||||
@@ -232,7 +253,12 @@ def main(argv: list[str] | None = None) -> int:
|
||||
raise SystemExit(
|
||||
"Collected installed evidence did not satisfy its bounded schema"
|
||||
)
|
||||
write_object(args.collect_installed_evidence, installed_evidence)
|
||||
write_object(
|
||||
args.collect_installed_evidence,
|
||||
installed_evidence,
|
||||
max_bytes=MAX_EVIDENCE_INPUT_BYTES,
|
||||
label="installed evidence",
|
||||
)
|
||||
|
||||
boundary_evidence = None
|
||||
boundary_evidence_schema = None
|
||||
@@ -260,6 +286,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
max_bytes=MAX_EVIDENCE_INPUT_BYTES,
|
||||
)
|
||||
|
||||
verification_time = parse_datetime(args.verification_time)
|
||||
report = review_capability_fit(
|
||||
assessment=assessment,
|
||||
schema=schema,
|
||||
@@ -275,12 +302,22 @@ def main(argv: list[str] | None = None) -> int:
|
||||
boundary_evidence_schema=boundary_evidence_schema,
|
||||
boundary_authority_keyring=boundary_authority_keyring,
|
||||
boundary_authority_keyring_schema=boundary_authority_keyring_schema,
|
||||
verification_time=parse_datetime(args.verification_time),
|
||||
verification_time=verification_time,
|
||||
installed_evidence_mode=(
|
||||
"direct_local"
|
||||
if args.collect_installed_evidence is not None
|
||||
else "imported_unsigned"
|
||||
),
|
||||
expected_external_provider_subject=args.expected_external_provider_subject,
|
||||
)
|
||||
encoded = json.dumps(report, indent=2, sort_keys=True) + "\n"
|
||||
if args.output is not None:
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(encoded, encoding="utf-8")
|
||||
write_object(
|
||||
args.output,
|
||||
report,
|
||||
max_bytes=MAX_REPORT_OUTPUT_BYTES,
|
||||
label="review report",
|
||||
)
|
||||
sys.stdout.write(encoded if args.json else render_review(report))
|
||||
return {"current": 0, "blocked": 1, "review_required": 2}[report["status"]]
|
||||
|
||||
@@ -307,16 +344,19 @@ def read_object(path: Path | None, *, label: str, max_bytes: int) -> dict[str, o
|
||||
return payload
|
||||
|
||||
|
||||
def write_object(path: Path | None, payload: dict[str, object]) -> None:
|
||||
def write_object(
|
||||
path: Path | None,
|
||||
payload: dict[str, object],
|
||||
*,
|
||||
max_bytes: int,
|
||||
label: str,
|
||||
) -> None:
|
||||
if path is None:
|
||||
raise SystemExit("Missing installed evidence output path")
|
||||
encoded = json.dumps(payload, indent=2, sort_keys=True) + "\n"
|
||||
if len(encoded.encode("utf-8")) > MAX_EVIDENCE_INPUT_BYTES:
|
||||
raise SystemExit(
|
||||
"Collected installed evidence exceeds the bounded output limit"
|
||||
)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(encoded, encoding="utf-8")
|
||||
raise SystemExit(f"Missing {label} output path")
|
||||
try:
|
||||
atomic_write_json(path, payload, max_bytes=max_bytes)
|
||||
except AtomicJsonWriteError as exc:
|
||||
raise SystemExit(f"Could not securely write {label}") from exc
|
||||
|
||||
|
||||
def parse_datetime(value: str | None):
|
||||
|
||||
230
tools/assessments/govoplan_assessment/atomic_io.py
Normal file
230
tools/assessments/govoplan_assessment/atomic_io.py
Normal file
@@ -0,0 +1,230 @@
|
||||
"""Bounded, private, atomic JSON output for assessment evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import stat
|
||||
import tempfile
|
||||
from typing import Any
|
||||
|
||||
|
||||
class AtomicJsonWriteError(RuntimeError):
|
||||
"""Raised when JSON output cannot be written with the required guarantees."""
|
||||
|
||||
|
||||
def atomic_write_json(
|
||||
path: str | os.PathLike[str],
|
||||
payload: Any,
|
||||
*,
|
||||
max_bytes: int,
|
||||
) -> None:
|
||||
"""Write bounded JSON through a private same-directory temporary file.
|
||||
|
||||
A successful return means the file contents and the containing directory
|
||||
entry have both been flushed. If flushing the directory fails after the
|
||||
atomic replacement, the replacement may be visible but its crash
|
||||
durability is unknown, so the function reports failure. Every parent must
|
||||
already exist as a real directory; symbolic-link components are rejected.
|
||||
"""
|
||||
|
||||
if isinstance(max_bytes, bool) or not isinstance(max_bytes, int) or max_bytes <= 0:
|
||||
raise ValueError("max_bytes must be a positive integer")
|
||||
|
||||
encoded = _encode_bounded_json(payload, max_bytes=max_bytes)
|
||||
target = Path(os.path.abspath(os.fspath(path)))
|
||||
parent = target.parent
|
||||
descriptor = -1
|
||||
verification_descriptor = -1
|
||||
temporary_path: Path | None = None
|
||||
temporary_identity: tuple[int, int] | None = None
|
||||
|
||||
try:
|
||||
_assert_real_parent(parent)
|
||||
_assert_replaceable_target(target)
|
||||
try:
|
||||
descriptor, temporary_name = tempfile.mkstemp(
|
||||
dir=parent,
|
||||
prefix=".govoplan-json-",
|
||||
suffix=".tmp",
|
||||
)
|
||||
temporary_path = Path(temporary_name)
|
||||
os.fchmod(descriptor, 0o600)
|
||||
metadata = os.fstat(descriptor)
|
||||
if (
|
||||
not stat.S_ISREG(metadata.st_mode)
|
||||
or stat.S_IMODE(metadata.st_mode) != 0o600
|
||||
):
|
||||
raise AtomicJsonWriteError(
|
||||
"Atomic JSON temporary file could not be secured."
|
||||
)
|
||||
temporary_identity = (metadata.st_dev, metadata.st_ino)
|
||||
verification_descriptor = os.dup(descriptor)
|
||||
|
||||
handle = os.fdopen(descriptor, "wb")
|
||||
descriptor = -1
|
||||
with handle:
|
||||
handle.write(encoded)
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
|
||||
# Recheck after creating and flushing the temporary file so an
|
||||
# unsafe target discovered before replacement is never followed.
|
||||
_assert_real_parent(parent)
|
||||
_assert_replaceable_target(target)
|
||||
os.replace(temporary_path, target)
|
||||
temporary_path = None
|
||||
_assert_installed_target(
|
||||
target,
|
||||
expected_identity=temporary_identity,
|
||||
recovery_descriptor=verification_descriptor,
|
||||
)
|
||||
_fsync_directory(parent)
|
||||
finally:
|
||||
try:
|
||||
if descriptor >= 0:
|
||||
os.close(descriptor)
|
||||
finally:
|
||||
try:
|
||||
if verification_descriptor >= 0:
|
||||
os.close(verification_descriptor)
|
||||
finally:
|
||||
if temporary_path is not None:
|
||||
try:
|
||||
temporary_path.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except AtomicJsonWriteError:
|
||||
raise
|
||||
except OSError as exc:
|
||||
raise AtomicJsonWriteError(
|
||||
"JSON output could not be written atomically."
|
||||
) from exc
|
||||
|
||||
|
||||
def _encode_bounded_json(payload: Any, *, max_bytes: int) -> bytes:
|
||||
encoder = json.JSONEncoder(
|
||||
allow_nan=False,
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
sort_keys=True,
|
||||
)
|
||||
chunks: list[bytes] = []
|
||||
encoded_size = 1 # The document always ends with one newline.
|
||||
for chunk in encoder.iterencode(payload):
|
||||
encoded_chunk = chunk.encode("utf-8")
|
||||
encoded_size += len(encoded_chunk)
|
||||
if encoded_size > max_bytes:
|
||||
raise AtomicJsonWriteError("JSON output exceeds its size limit.")
|
||||
chunks.append(encoded_chunk)
|
||||
return b"".join(chunks) + b"\n"
|
||||
|
||||
|
||||
def _assert_replaceable_target(path: Path) -> None:
|
||||
try:
|
||||
metadata = path.lstat()
|
||||
except FileNotFoundError:
|
||||
return
|
||||
except OSError as exc:
|
||||
raise AtomicJsonWriteError(
|
||||
"JSON output target could not be inspected safely."
|
||||
) from exc
|
||||
if stat.S_ISLNK(metadata.st_mode):
|
||||
raise AtomicJsonWriteError("JSON output target must not be a symbolic link.")
|
||||
if not stat.S_ISREG(metadata.st_mode):
|
||||
raise AtomicJsonWriteError("JSON output target must be a regular file.")
|
||||
|
||||
|
||||
def _assert_real_parent(path: Path) -> None:
|
||||
current = Path(path.anchor)
|
||||
for part in path.parts[1:]:
|
||||
current /= part
|
||||
try:
|
||||
metadata = current.lstat()
|
||||
except FileNotFoundError as exc:
|
||||
raise AtomicJsonWriteError(
|
||||
"JSON output parent directory must already exist."
|
||||
) from exc
|
||||
except OSError as exc:
|
||||
raise AtomicJsonWriteError(
|
||||
"JSON output parent directory could not be inspected safely."
|
||||
) from exc
|
||||
if stat.S_ISLNK(metadata.st_mode):
|
||||
raise AtomicJsonWriteError(
|
||||
"JSON output parent path must not contain symbolic links."
|
||||
)
|
||||
if not stat.S_ISDIR(metadata.st_mode):
|
||||
raise AtomicJsonWriteError(
|
||||
"JSON output parent path must contain only directories."
|
||||
)
|
||||
|
||||
|
||||
def _assert_installed_target(
|
||||
path: Path,
|
||||
*,
|
||||
expected_identity: tuple[int, int] | None,
|
||||
recovery_descriptor: int,
|
||||
) -> None:
|
||||
try:
|
||||
metadata = path.lstat()
|
||||
except OSError as exc:
|
||||
raise AtomicJsonWriteError(
|
||||
"Atomic JSON replacement could not be verified."
|
||||
) from exc
|
||||
observed_identity = (metadata.st_dev, metadata.st_ino)
|
||||
if (
|
||||
expected_identity is None
|
||||
or observed_identity != expected_identity
|
||||
or not stat.S_ISREG(metadata.st_mode)
|
||||
):
|
||||
raise AtomicJsonWriteError(
|
||||
"Atomic JSON replacement did not preserve the secured regular file."
|
||||
)
|
||||
if stat.S_IMODE(metadata.st_mode) == 0o600:
|
||||
return
|
||||
try:
|
||||
recovery_metadata = os.fstat(recovery_descriptor)
|
||||
if (
|
||||
not stat.S_ISREG(recovery_metadata.st_mode)
|
||||
or (recovery_metadata.st_dev, recovery_metadata.st_ino) != expected_identity
|
||||
):
|
||||
raise AtomicJsonWriteError(
|
||||
"Atomic JSON replacement recovery descriptor is invalid."
|
||||
)
|
||||
os.fchmod(recovery_descriptor, 0o600)
|
||||
os.fsync(recovery_descriptor)
|
||||
recovered_metadata = os.fstat(recovery_descriptor)
|
||||
installed_metadata = path.lstat()
|
||||
except OSError as exc:
|
||||
raise AtomicJsonWriteError(
|
||||
"Atomic JSON replacement permissions could not be restored."
|
||||
) from exc
|
||||
if (
|
||||
stat.S_IMODE(recovered_metadata.st_mode) != 0o600
|
||||
or (installed_metadata.st_dev, installed_metadata.st_ino) != expected_identity
|
||||
or not stat.S_ISREG(installed_metadata.st_mode)
|
||||
or stat.S_IMODE(installed_metadata.st_mode) != 0o600
|
||||
):
|
||||
raise AtomicJsonWriteError(
|
||||
"Atomic JSON replacement permissions could not be restored."
|
||||
)
|
||||
_fsync_directory(path.parent)
|
||||
raise AtomicJsonWriteError(
|
||||
"Atomic JSON replacement permissions changed and were restored."
|
||||
)
|
||||
|
||||
|
||||
def _fsync_directory(path: Path) -> None:
|
||||
flags = os.O_RDONLY
|
||||
if hasattr(os, "O_DIRECTORY"):
|
||||
flags |= os.O_DIRECTORY
|
||||
if hasattr(os, "O_NOFOLLOW"):
|
||||
flags |= os.O_NOFOLLOW
|
||||
descriptor = os.open(path, flags)
|
||||
try:
|
||||
if not stat.S_ISDIR(os.fstat(descriptor).st_mode):
|
||||
raise AtomicJsonWriteError("JSON output parent path must be a directory.")
|
||||
os.fsync(descriptor)
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
@@ -28,6 +28,7 @@ from govoplan_release.version_alignment import candidate_catalog_version_issues
|
||||
from .evidence import (
|
||||
BoundaryEvidenceReview,
|
||||
InstalledEvidenceReview,
|
||||
public_key_material_from_keyrings,
|
||||
review_boundary_evidence,
|
||||
review_installed_composition,
|
||||
)
|
||||
@@ -63,6 +64,8 @@ def review_capability_fit(
|
||||
boundary_authority_keyring: dict[str, Any] | None = None,
|
||||
boundary_authority_keyring_schema: dict[str, Any] | None = None,
|
||||
verification_time: datetime | None = None,
|
||||
installed_evidence_mode: str = "imported_unsigned",
|
||||
expected_external_provider_subject: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Return a secret-free release-drift report for one fit assessment."""
|
||||
|
||||
@@ -356,6 +359,10 @@ def review_capability_fit(
|
||||
)
|
||||
)
|
||||
|
||||
evidence_verification_time = verification_time or datetime.now(tz=UTC)
|
||||
evidence_verification_mode = (
|
||||
"historical_override" if verification_time is not None else "current"
|
||||
)
|
||||
installed_review = review_installed_composition(
|
||||
assessment=assessment,
|
||||
catalog_entries=catalog_entries,
|
||||
@@ -363,6 +370,9 @@ def review_capability_fit(
|
||||
selected_commits=source_selection.selected_commits,
|
||||
evidence=installed_evidence,
|
||||
schema=installed_evidence_schema,
|
||||
observation_mode=installed_evidence_mode,
|
||||
verification_time=evidence_verification_time,
|
||||
verification_mode=evidence_verification_mode,
|
||||
)
|
||||
findings.extend(
|
||||
Finding(item.severity, item.code, item.message, item.assessment_ids)
|
||||
@@ -378,7 +388,13 @@ def review_capability_fit(
|
||||
evidence_schema=boundary_evidence_schema,
|
||||
authority_keyring=boundary_authority_keyring,
|
||||
authority_keyring_schema=boundary_authority_keyring_schema,
|
||||
verification_time=verification_time,
|
||||
verification_time=evidence_verification_time,
|
||||
verification_mode=evidence_verification_mode,
|
||||
expected_external_provider_subject=expected_external_provider_subject,
|
||||
forbidden_authority_public_keys=public_key_material_from_keyrings(
|
||||
published_keyring,
|
||||
trusted_keyring,
|
||||
),
|
||||
)
|
||||
findings.extend(
|
||||
Finding(item.severity, item.code, item.message, item.assessment_ids)
|
||||
@@ -396,6 +412,7 @@ def review_capability_fit(
|
||||
assessment=assessment,
|
||||
installed_review=installed_review,
|
||||
),
|
||||
boundary_review.review_targets,
|
||||
)
|
||||
return build_report(
|
||||
assessment=assessment,
|
||||
@@ -908,7 +925,7 @@ def build_report(
|
||||
**boundary_scope,
|
||||
}
|
||||
return {
|
||||
"report_version": "0.2.0",
|
||||
"report_version": "0.3.0",
|
||||
"status": status,
|
||||
"assessment_id": assessment.get("assessment_id"),
|
||||
"assessment_release": assessment.get("release", {}).get("ref")
|
||||
@@ -930,9 +947,11 @@ def build_report(
|
||||
|
||||
|
||||
def render_review(report: dict[str, Any]) -> str:
|
||||
installed_checked = (
|
||||
report.get("proof_scope", {}).get("installed_artifacts", {}).get("checked")
|
||||
is True
|
||||
proof_scope = report.get("proof_scope", {})
|
||||
installed_scope = proof_scope.get("installed_artifacts", {})
|
||||
installed_checked = installed_scope.get("checked") is True
|
||||
installed_supplied = bool(installed_scope.get("evidence_sha256")) or (
|
||||
"installed_evidence_observation" in proof_scope
|
||||
)
|
||||
lines = [
|
||||
f"Capability fit rerun: {report['status']}",
|
||||
@@ -941,12 +960,29 @@ def render_review(report: dict[str, Any]) -> str:
|
||||
(
|
||||
"Scope: schema, signed release metadata, optional local tag provenance, "
|
||||
+ (
|
||||
"and supplied installed-composition evidence."
|
||||
"and locally observed installed-composition evidence."
|
||||
if installed_checked
|
||||
else "and no installed-composition evidence."
|
||||
else (
|
||||
"and supplied installed-composition evidence that did not establish local proof."
|
||||
if installed_supplied
|
||||
else "and no installed-composition evidence."
|
||||
)
|
||||
)
|
||||
),
|
||||
]
|
||||
verification = proof_scope.get("boundary_evidence_verification") or proof_scope.get(
|
||||
"installed_evidence_observation"
|
||||
)
|
||||
if isinstance(verification, dict) and verification.get("evaluated_at"):
|
||||
mode = verification.get("verification_mode")
|
||||
mode_label = (
|
||||
"HISTORICAL override"
|
||||
if mode == "historical_override"
|
||||
else "current-time evaluation"
|
||||
)
|
||||
lines.append(
|
||||
f"Evidence verification: {mode_label} at {verification['evaluated_at']}."
|
||||
)
|
||||
findings = report.get("findings") or []
|
||||
if findings:
|
||||
lines.append("Findings:")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user