Add value-safe verification and audit reporting
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a0217e-8c4c-7383-be6b-f50a6e485306
This commit is contained in:
parent
491e706a70
commit
c4504c6de9
19 changed files with 598 additions and 50 deletions
102
tests/test_audit.py
Normal file
102
tests/test_audit.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import json
|
||||
import urllib.error
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
from secrets_engine.audit import summarize_lane_evidence
|
||||
from secrets_engine.cli import build_parser
|
||||
from secrets_engine.evidence import EvidenceWriter
|
||||
|
||||
|
||||
DECISION_ID = "e6381a56-3e55-4fac-b22c-63ee1c152ce8"
|
||||
|
||||
|
||||
def test_lane_audit_summarizes_allowlisted_fields_and_cleanup(tmp_path, monkeypatch):
|
||||
writer = EvidenceWriter(
|
||||
evidence_dir=tmp_path,
|
||||
hub_url="http://hub.invalid",
|
||||
topic_id="topic-id",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"urllib.request.urlopen", lambda *_args, **_kwargs: SimpleNamespace(read=lambda: b"{}")
|
||||
)
|
||||
writer.record(
|
||||
"exec",
|
||||
result="exit-0",
|
||||
catalog_id="test-lane",
|
||||
stage="test",
|
||||
decision_id=DECISION_ID,
|
||||
detail={
|
||||
"session": {
|
||||
"session_handle": "safe-fingerprint",
|
||||
"revocation_attempted": True,
|
||||
"revocation_succeeded": True,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
def offline(*_args, **_kwargs):
|
||||
raise urllib.error.URLError("offline")
|
||||
|
||||
monkeypatch.setattr("urllib.request.urlopen", offline)
|
||||
writer.record(
|
||||
"verify",
|
||||
result="positive-pass",
|
||||
catalog_id="test-lane",
|
||||
stage="test",
|
||||
decision_id=DECISION_ID,
|
||||
detail={
|
||||
"session": {
|
||||
"session_handle": "another-safe-fingerprint",
|
||||
"revocation_attempted": True,
|
||||
"revocation_succeeded": False,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
path = next(tmp_path.glob("evidence-*.jsonl"))
|
||||
with path.open("a", encoding="utf-8") as fh:
|
||||
fh.write("not-json\n")
|
||||
fh.write(
|
||||
json.dumps(
|
||||
{
|
||||
"catalog_id": "test-lane",
|
||||
"action": "fake-SUPER-SECRET-value",
|
||||
"result": "fake-SUPER-SECRET-value",
|
||||
"decision_id": "fake-SUPER-SECRET-value",
|
||||
"ts": "not-a-time",
|
||||
"detail": {"value": "fake-SUPER-SECRET-value"},
|
||||
}
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
summary = summarize_lane_evidence(tmp_path, "test-lane")
|
||||
rendered = json.dumps(summary.to_json()) + summary.render()
|
||||
|
||||
assert summary.operation_records == 3
|
||||
assert summary.malformed_records == 1
|
||||
assert summary.actions == {"exec": 1, "invalid-label": 1, "verify": 1}
|
||||
assert summary.results == {
|
||||
"exit-0": 1,
|
||||
"invalid-label": 1,
|
||||
"positive-pass": 1,
|
||||
}
|
||||
assert summary.decision_refs == [DECISION_ID]
|
||||
assert summary.session_cleanup == {"failed": 1, "succeeded": 1}
|
||||
assert summary.hub_delivery == {"delivered": 1, "failed": 1}
|
||||
assert "fake-SUPER-SECRET-value" not in rendered
|
||||
assert "safe-fingerprint" not in rendered
|
||||
|
||||
|
||||
def test_lane_audit_empty_directory_is_a_valid_empty_summary(tmp_path):
|
||||
summary = summarize_lane_evidence(tmp_path, "test-lane")
|
||||
assert summary.operation_records == 0
|
||||
assert summary.actions == {}
|
||||
assert summary.render().startswith("Lane audit summary for 'test-lane'")
|
||||
|
||||
|
||||
def test_cli_parser_exposes_audit_json_command():
|
||||
args = build_parser().parse_args(["audit", "test-lane", "--json"])
|
||||
assert args.catalog_id == "test-lane"
|
||||
assert args.json is True
|
||||
|
|
@ -90,9 +90,32 @@ def test_full_chain(bao_dev, tmp_path):
|
|||
|
||||
pos = verify_positive(client, entry, "npm_token")
|
||||
assert pos.passed, pos.detail
|
||||
neg = verify_negative(client, entry)
|
||||
client.write_policy(
|
||||
"se-unrelated-denied",
|
||||
f'path "{entry.mount}/data/{entry.path}" {{ capabilities = ["deny"] }}\n',
|
||||
)
|
||||
unrelated = json.loads(
|
||||
client._run_ok(
|
||||
["token", "create", "-format=json", "-policy=se-unrelated-denied"]
|
||||
)
|
||||
)["auth"]["client_token"]
|
||||
neg = verify_negative(client, entry, unrelated_token=unrelated)
|
||||
assert neg.passed, neg.detail
|
||||
|
||||
# A real unrelated identity with accidental policy overlap must make the
|
||||
# negative check fail; an invalid/garbage token could not detect this.
|
||||
client.write_policy(
|
||||
"se-unrelated-overlap",
|
||||
f'path "{entry.mount}/data/{entry.path}" {{ capabilities = ["read"] }}\n',
|
||||
)
|
||||
overlapping = json.loads(
|
||||
client._run_ok(
|
||||
["token", "create", "-format=json", "-policy=se-unrelated-overlap"]
|
||||
)
|
||||
)["auth"]["client_token"]
|
||||
leaked = verify_negative(client, entry, unrelated_token=overlapping)
|
||||
assert not leaked.passed, leaked.detail
|
||||
|
||||
# exec delivery: child can resolve token via npmrc; assert via a probe script
|
||||
probe = tmp_path / "probe.sh"
|
||||
probe.write_text(
|
||||
|
|
|
|||
|
|
@ -83,8 +83,11 @@ def test_verify_defaults_to_every_declared_field_and_one_path_denial(
|
|||
monkeypatch.setattr(cli, "_require_lane_approval", lambda *_args: None)
|
||||
monkeypatch.setattr(cli.OpenBaoClient, "resolve", lambda *_args, **_kwargs: object())
|
||||
|
||||
def fake_verify(_client, _entry, field, *, positive, negative):
|
||||
def fake_verify(
|
||||
_client, _entry, field, *, positive, negative, unrelated_token=None
|
||||
):
|
||||
calls.append((field, positive, negative))
|
||||
assert unrelated_token is None
|
||||
check = "positive" if positive else "negative"
|
||||
return [VerifyResult(check, True, {"field": field, "reason": "test"})]
|
||||
|
||||
|
|
@ -100,6 +103,7 @@ def test_verify_defaults_to_every_declared_field_and_one_path_denial(
|
|||
field=None,
|
||||
positive=False,
|
||||
negative=False,
|
||||
negative_token_file=None,
|
||||
)
|
||||
|
||||
assert cli.cmd_verify(_config(tmp_path), args) == 0
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import json
|
||||
import urllib.error
|
||||
from types import SimpleNamespace
|
||||
|
||||
from secrets_engine.evidence import EvidenceWriter, _scrub
|
||||
from secrets_engine.redact import looks_secret, redact_text
|
||||
|
|
@ -39,3 +41,48 @@ def test_evidence_record_has_no_value(tmp_path):
|
|||
# written to disk too
|
||||
files = list(tmp_path.glob("evidence-*.jsonl"))
|
||||
assert files and "npm_shouldnotappear123" not in files[0].read_text()
|
||||
|
||||
|
||||
def test_evidence_records_append_only_hub_delivery_success(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"urllib.request.urlopen",
|
||||
lambda *_args, **_kwargs: SimpleNamespace(read=lambda: b"{}"),
|
||||
)
|
||||
writer = EvidenceWriter(
|
||||
evidence_dir=tmp_path,
|
||||
hub_url="http://hub.invalid",
|
||||
topic_id="topic-id",
|
||||
)
|
||||
|
||||
primary = writer.record("verify", result="pass", catalog_id="lane")
|
||||
|
||||
lines = [
|
||||
json.loads(line)
|
||||
for line in next(tmp_path.glob("evidence-*.jsonl")).read_text().splitlines()
|
||||
]
|
||||
assert len(lines) == 2
|
||||
assert lines[0] == primary
|
||||
assert lines[1]["action"] == "evidence-delivery"
|
||||
assert lines[1]["result"] == "delivered"
|
||||
assert lines[1]["related_record_id"] == primary["record_id"]
|
||||
|
||||
|
||||
def test_evidence_records_hub_failure_without_raising(tmp_path, monkeypatch):
|
||||
def offline(*_args, **_kwargs):
|
||||
raise urllib.error.URLError("offline")
|
||||
|
||||
monkeypatch.setattr("urllib.request.urlopen", offline)
|
||||
writer = EvidenceWriter(
|
||||
evidence_dir=tmp_path,
|
||||
hub_url="http://hub.invalid",
|
||||
topic_id="topic-id",
|
||||
)
|
||||
|
||||
writer.record("verify", result="pass", catalog_id="lane")
|
||||
|
||||
lines = [
|
||||
json.loads(line)
|
||||
for line in next(tmp_path.glob("evidence-*.jsonl")).read_text().splitlines()
|
||||
]
|
||||
assert lines[-1]["action"] == "evidence-delivery"
|
||||
assert lines[-1]["result"] == "failed"
|
||||
|
|
|
|||
61
tests/test_verification_negative.py
Normal file
61
tests/test_verification_negative.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import copy
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from secrets_engine.catalog import validate_entry
|
||||
from secrets_engine.errors import ProvisioningError
|
||||
from secrets_engine.openbao import read_strict_token_file
|
||||
from secrets_engine.verify import verify_negative
|
||||
from tests.test_catalog import VALID
|
||||
|
||||
|
||||
def _entry():
|
||||
return validate_entry(copy.deepcopy(VALID))
|
||||
|
||||
|
||||
class NegativeClient:
|
||||
def __init__(self, can_read):
|
||||
self.can_read = can_read
|
||||
self.tokens = []
|
||||
|
||||
def kv_can_read(self, _mount, _path, *, token):
|
||||
self.tokens.append(token)
|
||||
return self.can_read
|
||||
|
||||
|
||||
def test_negative_check_fails_closed_without_real_unrelated_token():
|
||||
client = NegativeClient(can_read=False)
|
||||
result = verify_negative(client, _entry(), unrelated_token=None)
|
||||
assert result.passed is False
|
||||
assert "not proven" in result.detail["reason"]
|
||||
assert client.tokens == []
|
||||
|
||||
|
||||
def test_negative_check_uses_real_token_and_detects_policy_overlap():
|
||||
denied = NegativeClient(can_read=False)
|
||||
assert verify_negative(
|
||||
denied, _entry(), unrelated_token="test-unrelated-token"
|
||||
).passed
|
||||
assert denied.tokens == ["test-unrelated-token"]
|
||||
|
||||
overlapping = NegativeClient(can_read=True)
|
||||
result = verify_negative(
|
||||
overlapping, _entry(), unrelated_token="test-overlapping-token"
|
||||
)
|
||||
assert result.passed is False
|
||||
assert "LEAK RISK" in result.detail["reason"]
|
||||
|
||||
|
||||
def test_negative_token_file_requires_mode_0600(tmp_path):
|
||||
path = tmp_path / "unrelated.token"
|
||||
path.write_text("test-unrelated-token", encoding="utf-8")
|
||||
os.chmod(path, 0o644)
|
||||
with pytest.raises(ProvisioningError, match="must be 0600"):
|
||||
read_strict_token_file(path, purpose="negative verification token")
|
||||
|
||||
os.chmod(path, 0o600)
|
||||
assert (
|
||||
read_strict_token_file(path, purpose="negative verification token")
|
||||
== "test-unrelated-token"
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue