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: 3 additions & 1 deletion src/github_repo_auditor/github_security_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
DEFAULT_ATTENTION_STATES = frozenset(
{"active-product", "active-infra", "decision-needed"}
)
DEFAULT_EXPECTED_GITHUB_COHORT_COUNT = 11
# Owner-confirmed 2026-08-21 after agent-session-replay legitimately entered
# decision-needed while remaining on its active finish path.
DEFAULT_EXPECTED_GITHUB_COHORT_COUNT = 12
PROVIDER_NAMES = ("dependabot", "code_scanning", "secret_scanning")
ELIGIBILITY_SOURCE = "github-account-repository-preflight-v1"
ELIGIBILITY_REASON = "private_user_repo_plan_unavailable"
Expand Down
39 changes: 35 additions & 4 deletions src/github_repo_auditor/portfolio_truth_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -1887,19 +1887,50 @@ def canonicalize_prior_security_truth_payload(
*,
security_max_age_hours: int = 24,
) -> dict[str, Any]:
"""Canonicalize prior security evidence across one bounded metadata upgrade.
"""Canonicalize prior security evidence across bounded metadata upgrades.

Current snapshots always take the ordinary strict path. The compatibility
path accepts only the immediately preceding discovery envelope, before
checkout-collision metadata existed, and reconstructs those two unrelated
metadata envelopes in memory before running the complete current validator.
paths accept only the immediately preceding additive schema or the earlier
discovery envelope before checkout-collision metadata existed. Each path
reconstructs only its named metadata in memory before running the complete
current validator.
"""
try:
return canonicalize_truth_snapshot_payload(
payload,
security_max_age_hours=security_max_age_hours,
)
except ValueError as strict_error:
if payload.get("schema_version") == "0.11.0" and SCHEMA_VERSION == "0.12.0":
migrated = deepcopy(dict(payload))
projects = migrated.get("projects")
if not isinstance(projects, list):
raise ValueError(
"Prior PortfolioTruth 0.11.0 project envelope is invalid."
) from strict_error
for raw_project in projects:
if not isinstance(raw_project, dict) or not isinstance(
raw_project.get("derived"), dict
):
raise ValueError(
"Prior PortfolioTruth 0.11.0 project envelope is invalid."
) from strict_error
derived = raw_project["derived"]
if "degraded_dimensions" in derived:
raise ValueError(
"Prior PortfolioTruth 0.11.0 cannot declare "
"derived.degraded_dimensions."
) from strict_error
derived["degraded_dimensions"] = None
migrated["schema_version"] = SCHEMA_VERSION
try:
return canonicalize_truth_snapshot_payload(
migrated,
security_max_age_hours=security_max_age_hours,
)
except ValueError as migration_error:
raise migration_error from strict_error

summary = payload.get("source_summary")
exclusions = payload.get("exclusions")
is_bounded_legacy_payload = (
Expand Down
7 changes: 5 additions & 2 deletions tests/test_github_security_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,7 @@ def _remote_graphql_response(count: int) -> _Response:


def test_default_attention_cohort_is_exact_and_fail_closed() -> None:
assert DEFAULT_EXPECTED_GITHUB_COHORT_COUNT == 12
truth = _truth(DEFAULT_EXPECTED_GITHUB_COHORT_COUNT)
truth["projects"].append(
{
Expand All @@ -759,8 +760,10 @@ def test_default_attention_cohort_is_exact_and_fail_closed() -> None:

assert len(cohort) == DEFAULT_EXPECTED_GITHUB_COHORT_COUNT
assert "owner/parked" not in cohort
with pytest.raises(SecurityCoverageError, match="expected 11, observed 12"):
derive_default_attention_cohort(_truth(12))
with pytest.raises(SecurityCoverageError, match="expected 12, observed 13"):
derive_default_attention_cohort(
_truth(DEFAULT_EXPECTED_GITHUB_COHORT_COUNT + 1)
)


def test_repo_less_non_supplementary_attention_identity_fails_closed() -> None:
Expand Down
54 changes: 51 additions & 3 deletions tests/test_portfolio_truth.py
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ def test_prior_security_loader_accepts_bounded_legacy_truth(
portfolio_catalog: Path,
legacy_registry: Path,
) -> None:
import github_repo_auditor.portfolio_truth_publish as publish_mod
from github_repo_auditor import portfolio_truth_publish as publish_mod

payload, metadata, _ = _legacy_prior_security_payload(
portfolio_workspace=portfolio_workspace,
Expand Down Expand Up @@ -715,7 +715,7 @@ def test_prior_security_loader_allows_same_receipt_truth_generated_after_receipt
portfolio_catalog: Path,
legacy_registry: Path,
) -> None:
import github_repo_auditor.portfolio_truth_publish as publish_mod
from github_repo_auditor import portfolio_truth_publish as publish_mod

_, metadata, payload = _legacy_prior_security_payload(
portfolio_workspace=portfolio_workspace,
Expand All @@ -736,6 +736,54 @@ def test_prior_security_loader_allows_same_receipt_truth_generated_after_receipt
assert evidence.alerts_by_full_name["d/Alpha"]["dependabot_high"] == 1


def test_prior_security_loader_accepts_immediate_additive_schema_predecessor(
tmp_path: Path,
portfolio_workspace: Path,
portfolio_catalog: Path,
legacy_registry: Path,
) -> None:
from github_repo_auditor import portfolio_truth_publish as publish_mod

_, metadata, payload = _legacy_prior_security_payload(
portfolio_workspace=portfolio_workspace,
portfolio_catalog=portfolio_catalog,
legacy_registry=legacy_registry,
)
payload["schema_version"] = "0.11.0"
for project in payload["projects"]:
project["derived"].pop("degraded_dimensions")
latest = tmp_path / "portfolio-truth-latest.json"
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
latest.write_text(json.dumps(payload), encoding="utf-8")

evidence = publish_mod._load_prior_security_alerts(
latest,
current_security_metadata=metadata,
security_max_age_hours=24,
)

assert evidence.final_cohort_repositories == ("d/Alpha",)
assert evidence.alerts_by_full_name["d/Alpha"]["dependabot_high"] == 1


def test_immediate_additive_schema_predecessor_rejects_new_field(
portfolio_workspace: Path,
portfolio_catalog: Path,
legacy_registry: Path,
) -> None:
_, _, payload = _legacy_prior_security_payload(
portfolio_workspace=portfolio_workspace,
portfolio_catalog=portfolio_catalog,
legacy_registry=legacy_registry,
)
payload["schema_version"] = "0.11.0"

with pytest.raises(
ValueError,
match="0.11.0 cannot declare derived.degraded_dimensions",
):
canonicalize_prior_security_truth_payload(payload)


@pytest.mark.parametrize(
("binding_field", "replacement"),
(
Expand All @@ -752,7 +800,7 @@ def test_prior_security_loader_refuses_future_truth_from_different_receipt(
portfolio_catalog: Path,
legacy_registry: Path,
) -> None:
import github_repo_auditor.portfolio_truth_publish as publish_mod
from github_repo_auditor import portfolio_truth_publish as publish_mod

_, metadata, payload = _legacy_prior_security_payload(
portfolio_workspace=portfolio_workspace,
Expand Down