agentic-security-harness 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. agentic_security_harness/__init__.py +220 -0
  2. agentic_security_harness/adapter_base.py +61 -0
  3. agentic_security_harness/adapters.py +197 -0
  4. agentic_security_harness/attestation_policy.py +203 -0
  5. agentic_security_harness/cli.py +4752 -0
  6. agentic_security_harness/companion_contracts.py +1272 -0
  7. agentic_security_harness/context_consent_campaign.py +730 -0
  8. agentic_security_harness/corpus.py +558 -0
  9. agentic_security_harness/covering_array.py +145 -0
  10. agentic_security_harness/demo_adapter.py +719 -0
  11. agentic_security_harness/demo_agent.py +551 -0
  12. agentic_security_harness/doctor.py +306 -0
  13. agentic_security_harness/envelope_policy.py +145 -0
  14. agentic_security_harness/evidence_campaign.py +918 -0
  15. agentic_security_harness/evidence_quality.py +1141 -0
  16. agentic_security_harness/evidence_status.py +383 -0
  17. agentic_security_harness/external_openai_compatible.py +220 -0
  18. agentic_security_harness/external_prompt.py +88 -0
  19. agentic_security_harness/external_runner.py +958 -0
  20. agentic_security_harness/external_validation.py +121 -0
  21. agentic_security_harness/ground_truth.py +116 -0
  22. agentic_security_harness/handoff_integrity.py +351 -0
  23. agentic_security_harness/html_report.py +620 -0
  24. agentic_security_harness/local_profiles.py +180 -0
  25. agentic_security_harness/local_swarm.py +1142 -0
  26. agentic_security_harness/local_swarm_matrix.py +1396 -0
  27. agentic_security_harness/marketing_web_injection_campaign.py +918 -0
  28. agentic_security_harness/marketing_web_live_campaign.py +1186 -0
  29. agentic_security_harness/matrix.py +407 -0
  30. agentic_security_harness/memory_governance.py +136 -0
  31. agentic_security_harness/memory_rehydration_campaign.py +1033 -0
  32. agentic_security_harness/mock_target.py +190 -0
  33. agentic_security_harness/models.py +237 -0
  34. agentic_security_harness/patterns.py +695 -0
  35. agentic_security_harness/planner_task_campaign.py +1033 -0
  36. agentic_security_harness/portfolio_contract.py +552 -0
  37. agentic_security_harness/presets.py +298 -0
  38. agentic_security_harness/protected_demo_agent.py +484 -0
  39. agentic_security_harness/py.typed +1 -0
  40. agentic_security_harness/r5_public_projection.py +633 -0
  41. agentic_security_harness/rag_context_campaign.py +1020 -0
  42. agentic_security_harness/reconciliation.py +869 -0
  43. agentic_security_harness/remediation.py +808 -0
  44. agentic_security_harness/reporting.py +276 -0
  45. agentic_security_harness/reviewer_receipt.py +112 -0
  46. agentic_security_harness/run_config.py +278 -0
  47. agentic_security_harness/run_diff.py +487 -0
  48. agentic_security_harness/run_manifest.py +336 -0
  49. agentic_security_harness/rundb.py +320 -0
  50. agentic_security_harness/runner.py +133 -0
  51. agentic_security_harness/runtime_guard_foundation.py +1203 -0
  52. agentic_security_harness/safe_io.py +442 -0
  53. agentic_security_harness/safe_markdown.py +78 -0
  54. agentic_security_harness/safe_terminal.py +46 -0
  55. agentic_security_harness/scenario_adjudication.py +285 -0
  56. agentic_security_harness/scenario_timeline.py +297 -0
  57. agentic_security_harness/scenarios.py +528 -0
  58. agentic_security_harness/schema_versions.py +156 -0
  59. agentic_security_harness/scorecard.py +46 -0
  60. agentic_security_harness/secret_hygiene.py +126 -0
  61. agentic_security_harness/secret_leak_campaign.py +1456 -0
  62. agentic_security_harness/semantic_drift_campaign.py +913 -0
  63. agentic_security_harness/semantic_propagation_campaign.py +1229 -0
  64. agentic_security_harness/showcase.py +766 -0
  65. agentic_security_harness/source_identity.py +44 -0
  66. agentic_security_harness/standards_mapping.py +254 -0
  67. agentic_security_harness/static_import_closure.py +509 -0
  68. agentic_security_harness/stats.py +493 -0
  69. agentic_security_harness/swarm_defense_contour.py +585 -0
  70. agentic_security_harness/swarm_defense_live_campaign.py +1258 -0
  71. agentic_security_harness/swarm_resilience_campaign.py +983 -0
  72. agentic_security_harness/tool_authority_campaign.py +890 -0
  73. agentic_security_harness/toy_adapters.py +472 -0
  74. agentic_security_harness/trace_schema.py +49 -0
  75. agentic_security_harness/trading_bot_stand.py +4866 -0
  76. agentic_security_harness/validation.py +6796 -0
  77. agentic_security_harness/version.py +3 -0
  78. agentic_security_harness-1.0.0.dist-info/METADATA +286 -0
  79. agentic_security_harness-1.0.0.dist-info/RECORD +84 -0
  80. agentic_security_harness-1.0.0.dist-info/WHEEL +5 -0
  81. agentic_security_harness-1.0.0.dist-info/entry_points.txt +2 -0
  82. agentic_security_harness-1.0.0.dist-info/licenses/LICENSE +201 -0
  83. agentic_security_harness-1.0.0.dist-info/licenses/NOTICE +10 -0
  84. agentic_security_harness-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,220 @@
1
+ """Agentic Security Harness - defensive benchmark core.
2
+
3
+ Pipeline: ``pattern -> target -> trace -> scorecard``. Built-in/local targets are
4
+ deterministic and offline (no network, no provider calls). The experimental
5
+ ``run-external --execute`` makes OpenAI-compatible calls only on explicit opt-in; native
6
+ provider and agent-host adapters are future. A ``DataEnvelope`` is a policy label, not
7
+ encryption.
8
+ """
9
+
10
+ from agentic_security_harness.adapter_base import TargetAdapterBase
11
+ from agentic_security_harness.corpus import (
12
+ CORPUS_MANIFEST_SCHEMA_VERSION,
13
+ DEPRECATED_PATTERN_IDS,
14
+ PATTERN_REPLACEMENTS,
15
+ V1_PATTERN_IDS,
16
+ CorpusEntry,
17
+ CorpusManifestV1,
18
+ CorpusPatternReplacement,
19
+ corpus_contract,
20
+ corpus_manifest,
21
+ corpus_manifest_sha256,
22
+ parse_corpus_contract_json,
23
+ parse_corpus_contract_payload,
24
+ )
25
+ from agentic_security_harness.demo_adapter import DemoAgentTarget, run_scenarios
26
+ from agentic_security_harness.demo_agent import DemoAgent
27
+ from agentic_security_harness.doctor import DoctorReport, run_doctor
28
+ from agentic_security_harness.html_report import render_report, write_html_report
29
+ from agentic_security_harness.local_swarm import (
30
+ LocalSwarmSummary,
31
+ SwarmMetrics,
32
+ SwarmScenarioResult,
33
+ estimate_request_count,
34
+ run_local_swarm,
35
+ )
36
+ from agentic_security_harness.mock_target import MockTarget
37
+ from agentic_security_harness.models import (
38
+ AuditEntry,
39
+ CapabilityCheckResult,
40
+ CapabilityToken,
41
+ DataEnvelope,
42
+ DefensivePattern,
43
+ ExploitTrace,
44
+ Finding,
45
+ HealthStatus,
46
+ Observation,
47
+ Target,
48
+ TargetDescriptor,
49
+ TargetMetadata,
50
+ ToolSchemaRecord,
51
+ TraceStep,
52
+ )
53
+ from agentic_security_harness.patterns import seed_patterns
54
+ from agentic_security_harness.presets import Preset, apply_preset, list_presets
55
+ from agentic_security_harness.protected_demo_agent import (
56
+ ProtectedDemoAgent,
57
+ ProtectedDemoAgentTarget,
58
+ )
59
+ from agentic_security_harness.remediation import (
60
+ ControlRecommendation,
61
+ RemediationReport,
62
+ build_recommendations,
63
+ build_remediation_md,
64
+ remediation_to_json,
65
+ write_remediation,
66
+ )
67
+ from agentic_security_harness.reporting import (
68
+ build_comparison_md,
69
+ build_executive_md,
70
+ build_summary_md,
71
+ scorecard_to_json,
72
+ traces_to_json,
73
+ write_comparison,
74
+ write_reports,
75
+ )
76
+ from agentic_security_harness.run_diff import RunDiff, diff_runs, write_run_diff
77
+ from agentic_security_harness.run_manifest import (
78
+ RunManifest,
79
+ build_manifest,
80
+ load_run_manifests,
81
+ write_run_manifest,
82
+ )
83
+ from agentic_security_harness.rundb import index_runs, list_db_runs
84
+ from agentic_security_harness.runner import HarnessRunner
85
+ from agentic_security_harness.scenario_timeline import (
86
+ ScenarioTimeline,
87
+ TimelineReplay,
88
+ TimelineReplayStep,
89
+ TimelineValidatorResult,
90
+ replay_timeline,
91
+ validate_timeline,
92
+ )
93
+ from agentic_security_harness.schema_versions import (
94
+ SCHEMA_VERSIONS,
95
+ check_schema_version,
96
+ schema_version,
97
+ )
98
+ from agentic_security_harness.scorecard import ScorecardSummary, build_scorecard
99
+ from agentic_security_harness.standards_mapping import (
100
+ CategoryStandards,
101
+ standards_mapping,
102
+ validate_standards_mapping,
103
+ )
104
+ from agentic_security_harness.stats import (
105
+ RetentionCandidate,
106
+ RetentionPlan,
107
+ RunStats,
108
+ apply_retention_plan,
109
+ build_retention_plan,
110
+ build_run_stats,
111
+ write_run_stats,
112
+ )
113
+ from agentic_security_harness.trace_schema import (
114
+ TRACE_SCHEMA_LEGACY_VERSIONS,
115
+ TRACE_SCHEMA_V1,
116
+ migrate_trace_payload_to_v1,
117
+ parse_trace_payload,
118
+ )
119
+ from agentic_security_harness.validation import ValidationResult, validate_path
120
+ from agentic_security_harness.version import __version__
121
+
122
+ __all__ = [
123
+ "__version__",
124
+ "DataEnvelope",
125
+ "CapabilityToken",
126
+ "CapabilityCheckResult",
127
+ "ToolSchemaRecord",
128
+ "AuditEntry",
129
+ "DefensivePattern",
130
+ "ExploitTrace",
131
+ "Finding",
132
+ "HealthStatus",
133
+ "Observation",
134
+ "Target",
135
+ "TargetAdapterBase",
136
+ "TargetDescriptor",
137
+ "TargetMetadata",
138
+ "TraceStep",
139
+ "MockTarget",
140
+ "DemoAgent",
141
+ "DemoAgentTarget",
142
+ "ProtectedDemoAgent",
143
+ "ProtectedDemoAgentTarget",
144
+ "run_scenarios",
145
+ "HarnessRunner",
146
+ "ScorecardSummary",
147
+ "SCHEMA_VERSIONS",
148
+ "schema_version",
149
+ "check_schema_version",
150
+ "TRACE_SCHEMA_V1",
151
+ "TRACE_SCHEMA_LEGACY_VERSIONS",
152
+ "parse_trace_payload",
153
+ "migrate_trace_payload_to_v1",
154
+ "RunDiff",
155
+ "diff_runs",
156
+ "write_run_diff",
157
+ "ScenarioTimeline",
158
+ "TimelineReplay",
159
+ "TimelineReplayStep",
160
+ "TimelineValidatorResult",
161
+ "replay_timeline",
162
+ "validate_timeline",
163
+ "Preset",
164
+ "list_presets",
165
+ "apply_preset",
166
+ "index_runs",
167
+ "list_db_runs",
168
+ "build_scorecard",
169
+ "seed_patterns",
170
+ "CorpusEntry",
171
+ "CorpusManifestV1",
172
+ "CorpusPatternReplacement",
173
+ "CORPUS_MANIFEST_SCHEMA_VERSION",
174
+ "V1_PATTERN_IDS",
175
+ "DEPRECATED_PATTERN_IDS",
176
+ "PATTERN_REPLACEMENTS",
177
+ "corpus_contract",
178
+ "corpus_manifest",
179
+ "corpus_manifest_sha256",
180
+ "parse_corpus_contract_json",
181
+ "parse_corpus_contract_payload",
182
+ "write_reports",
183
+ "write_comparison",
184
+ "build_summary_md",
185
+ "build_executive_md",
186
+ "build_comparison_md",
187
+ "traces_to_json",
188
+ "scorecard_to_json",
189
+ "ValidationResult",
190
+ "validate_path",
191
+ "RunManifest",
192
+ "build_manifest",
193
+ "write_run_manifest",
194
+ "load_run_manifests",
195
+ "render_report",
196
+ "write_html_report",
197
+ "LocalSwarmSummary",
198
+ "SwarmMetrics",
199
+ "SwarmScenarioResult",
200
+ "run_local_swarm",
201
+ "estimate_request_count",
202
+ "run_doctor",
203
+ "DoctorReport",
204
+ "CategoryStandards",
205
+ "standards_mapping",
206
+ "validate_standards_mapping",
207
+ "RunStats",
208
+ "RetentionCandidate",
209
+ "RetentionPlan",
210
+ "build_run_stats",
211
+ "write_run_stats",
212
+ "build_retention_plan",
213
+ "apply_retention_plan",
214
+ "ControlRecommendation",
215
+ "RemediationReport",
216
+ "build_recommendations",
217
+ "build_remediation_md",
218
+ "remediation_to_json",
219
+ "write_remediation",
220
+ ]
@@ -0,0 +1,61 @@
1
+ """Base lifecycle hooks for target adapters.
2
+
3
+ Adapters can still satisfy the small structural ``Target`` protocol directly. This base
4
+ class exists for adapters that want explicit readiness, reproducibility metadata, and
5
+ per-pattern capability checks without re-implementing boilerplate.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from agentic_security_harness.models import (
11
+ CapabilityCheckResult,
12
+ DefensivePattern,
13
+ HealthStatus,
14
+ TargetMetadata,
15
+ )
16
+
17
+
18
+ class TargetAdapterBase:
19
+ """Convenience base class for target adapters with lifecycle hooks."""
20
+
21
+ target_type = "custom"
22
+ adapter_name = "custom"
23
+ adapter_version = "0.1"
24
+ deterministic = True
25
+ network_mode = "off"
26
+ memory_mode = "off"
27
+ permission_model = "none"
28
+ provider_calls = False
29
+
30
+ def __init__(self, name: str | None = None) -> None:
31
+ self.name = name or self.adapter_name
32
+
33
+ def descriptor_fields(self) -> tuple[str, str, str]:
34
+ return (self.target_type, self.name, self.adapter_name)
35
+
36
+ def health(self) -> HealthStatus:
37
+ return HealthStatus(
38
+ ok=True,
39
+ status="ready",
40
+ message=f"{self.adapter_name} adapter ready",
41
+ checks={"descriptor": True, "observe": True},
42
+ )
43
+
44
+ def metadata(self, run_id: str) -> TargetMetadata:
45
+ return TargetMetadata(
46
+ adapter_name=self.adapter_name,
47
+ adapter_version=self.adapter_version,
48
+ memory_mode=self.memory_mode, # type: ignore[arg-type]
49
+ permission_model=self.permission_model, # type: ignore[arg-type]
50
+ network_mode=self.network_mode, # type: ignore[arg-type]
51
+ deterministic=self.deterministic,
52
+ provider_calls=self.provider_calls,
53
+ run_id=run_id,
54
+ )
55
+
56
+ def capability_check(self, pattern: DefensivePattern) -> CapabilityCheckResult:
57
+ return CapabilityCheckResult(
58
+ pattern_id=pattern.pattern_id,
59
+ supported=True,
60
+ safety_gates_passed=True,
61
+ )
@@ -0,0 +1,197 @@
1
+ """Adapter registry: maps target ids to constructors and metadata.
2
+
3
+ Built-in targets are local, deterministic, and make no network or provider calls.
4
+ This module provides ``list_targets()`` and ``make_target()`` for CLI discovery.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from agentic_security_harness.models import Target
12
+
13
+
14
+ class TargetInfo:
15
+ """Metadata for a registered built-in target."""
16
+
17
+ __slots__ = ("target_id", "name", "type", "deterministic", "description", "_factory")
18
+
19
+ def __init__(
20
+ self,
21
+ target_id: str,
22
+ name: str,
23
+ type: str,
24
+ deterministic: bool,
25
+ description: str,
26
+ factory: Any,
27
+ ) -> None:
28
+ self.target_id = target_id
29
+ self.name = name
30
+ self.type = type
31
+ self.deterministic = deterministic
32
+ self.description = description
33
+ self._factory = factory
34
+
35
+ def make(self) -> Target:
36
+ return self._factory()
37
+
38
+
39
+ _REGISTRY: list[TargetInfo] = []
40
+
41
+
42
+ def _register(info: TargetInfo) -> None:
43
+ _REGISTRY.append(info)
44
+
45
+
46
+ def _init_registry() -> None:
47
+ if _REGISTRY:
48
+ return
49
+
50
+ from agentic_security_harness.demo_adapter import DemoAgentTarget
51
+ from agentic_security_harness.mock_target import MockTarget
52
+ from agentic_security_harness.protected_demo_agent import ProtectedDemoAgentTarget
53
+
54
+ _register(
55
+ TargetInfo(
56
+ target_id="mock",
57
+ name="demo-mock-agent",
58
+ type="mock_agent",
59
+ deterministic=True,
60
+ description="Deterministic minimal target for fast benchmark checks",
61
+ factory=MockTarget,
62
+ )
63
+ )
64
+ _register(
65
+ TargetInfo(
66
+ target_id="demo-agent",
67
+ name="demo-local-agent",
68
+ type="demo_agent",
69
+ deterministic=True,
70
+ description="Local vulnerable-by-design synthetic agent",
71
+ factory=DemoAgentTarget,
72
+ )
73
+ )
74
+ _register(
75
+ TargetInfo(
76
+ target_id="protected-demo-agent",
77
+ name="protected-demo-agent",
78
+ type="protected_demo_agent",
79
+ deterministic=True,
80
+ description="Local controlled synthetic agent; demonstrates risk reduction",
81
+ factory=ProtectedDemoAgentTarget,
82
+ )
83
+ )
84
+ _register(
85
+ TargetInfo(
86
+ target_id="toy-local-function",
87
+ name="toy-local-function",
88
+ type="toy_local",
89
+ deterministic=True,
90
+ description="Toy adapter wrapping a plain Python function as a target",
91
+ factory=_ToyLocalFunctionTarget,
92
+ )
93
+ )
94
+
95
+ from agentic_security_harness.toy_adapters import (
96
+ ProtectedToyMultiAgentHandoffTarget,
97
+ ToyMultiAgentHandoffTarget,
98
+ ToyRagTarget,
99
+ ToyToolsTarget,
100
+ )
101
+
102
+ _register(
103
+ TargetInfo(
104
+ target_id="toy-rag",
105
+ name="toy-rag",
106
+ type="toy_rag",
107
+ deterministic=True,
108
+ description="Toy retrieval agent; exercises data/memory/injection surfaces",
109
+ factory=ToyRagTarget,
110
+ )
111
+ )
112
+ _register(
113
+ TargetInfo(
114
+ target_id="toy-tools",
115
+ name="toy-tools",
116
+ type="toy_tools",
117
+ deterministic=True,
118
+ description="Toy tool-using agent; exercises tool/authority surfaces",
119
+ factory=ToyToolsTarget,
120
+ )
121
+ )
122
+ _register(
123
+ TargetInfo(
124
+ target_id="toy-multi-agent",
125
+ name="toy-multi-agent",
126
+ type="toy_multi_agent",
127
+ deterministic=True,
128
+ description=(
129
+ "Toy coordinator/worker handoff adapter; exercises data-label "
130
+ "handoff and capability delegation surfaces"
131
+ ),
132
+ factory=ToyMultiAgentHandoffTarget,
133
+ )
134
+ )
135
+ _register(
136
+ TargetInfo(
137
+ target_id="protected-toy-multi-agent",
138
+ name="protected-toy-multi-agent",
139
+ type="protected_toy_multi_agent",
140
+ deterministic=True,
141
+ description=(
142
+ "Protected toy coordinator/worker handoff adapter; blocks malformed "
143
+ "handoffs through deterministic verifier decisions"
144
+ ),
145
+ factory=ProtectedToyMultiAgentHandoffTarget,
146
+ )
147
+ )
148
+
149
+
150
+ class _ToyLocalFunctionTarget:
151
+ """Toy adapter: wraps a deterministic Python function as a target.
152
+
153
+ Returns PASS (no findings) for every pattern, useful for docs and tests.
154
+ """
155
+
156
+ def __init__(self, name: str = "toy-local-function") -> None:
157
+ self.name = name
158
+
159
+ def descriptor_fields(self) -> tuple[str, str, str]:
160
+ return ("toy_local", self.name, "toy")
161
+
162
+ def observe(self, pattern: Any) -> Any:
163
+ from agentic_security_harness.models import Observation, TraceStep
164
+
165
+ steps = [
166
+ TraceStep(index=i, actor="toy", action=node)
167
+ for i, node in enumerate(pattern.graph_path)
168
+ ]
169
+ return Observation(
170
+ steps=steps,
171
+ observed_behavior="toy adapter completed with no observed violation",
172
+ findings=[],
173
+ )
174
+
175
+
176
+ def list_targets() -> list[TargetInfo]:
177
+ """Return metadata for all registered built-in targets."""
178
+ _init_registry()
179
+ return list(_REGISTRY)
180
+
181
+
182
+ def make_target(target_id: str) -> Target:
183
+ """Create a target instance by its registered id.
184
+
185
+ Raises ``KeyError`` with a helpful message if the id is unknown.
186
+ """
187
+ _init_registry()
188
+ for info in _REGISTRY:
189
+ if info.target_id == target_id:
190
+ return info.make()
191
+ known = ", ".join(info.target_id for info in _REGISTRY)
192
+ raise KeyError(f"unknown target id '{target_id}'. Known targets: {known}")
193
+
194
+
195
+ def target_ids() -> list[str]:
196
+ """Return just the registered target ids."""
197
+ return [info.target_id for info in list_targets()]
@@ -0,0 +1,203 @@
1
+ """Fail-closed policy checks for GitHub artifact-attestation verification output.
2
+
3
+ Cryptographic verification remains the responsibility of ``gh attestation verify``.
4
+ This module checks the authenticated result against the release policy that the CLI's
5
+ generic signature verification cannot infer for this repository.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import hashlib
12
+ import json
13
+ from collections.abc import Mapping, Sequence
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ SLSA_PROVENANCE_V1 = "https://slsa.dev/provenance/v1"
18
+ GITHUB_WORKFLOW_BUILD_TYPE = "https://actions.github.io/buildtypes/workflow/v1"
19
+ IN_TOTO_STATEMENT_V1 = "https://in-toto.io/Statement/v1"
20
+
21
+
22
+ def _mapping(value: object) -> Mapping[str, Any] | None:
23
+ return value if isinstance(value, Mapping) else None
24
+
25
+
26
+ def _sequence(value: object) -> Sequence[object] | None:
27
+ if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
28
+ return value
29
+ return None
30
+
31
+
32
+ def _entry_issues(
33
+ entry: object,
34
+ *,
35
+ repository: str,
36
+ workflow_path: str,
37
+ source_ref: str,
38
+ source_digest: str,
39
+ subject_sha256: str,
40
+ ) -> list[str]:
41
+ issues: list[str] = []
42
+ row = _mapping(entry)
43
+ if row is None:
44
+ return ["verification entry is not an object"]
45
+
46
+ result = _mapping(row.get("verificationResult"))
47
+ if result is None:
48
+ return ["verificationResult is missing"]
49
+ timestamps = _sequence(result.get("verifiedTimestamps"))
50
+ if not timestamps:
51
+ issues.append("verified transparency/timestamp evidence is missing")
52
+
53
+ statement = _mapping(result.get("statement"))
54
+ if statement is None:
55
+ return [*issues, "verified statement is missing"]
56
+ if statement.get("_type") != IN_TOTO_STATEMENT_V1:
57
+ issues.append("statement type is not in-toto Statement v1")
58
+ if statement.get("predicateType") != SLSA_PROVENANCE_V1:
59
+ issues.append("predicate type is not SLSA provenance v1")
60
+
61
+ subjects = _sequence(statement.get("subject"))
62
+ subject_match = False
63
+ if subjects is not None:
64
+ for subject in subjects:
65
+ subject_row = _mapping(subject)
66
+ digest = _mapping(subject_row.get("digest")) if subject_row else None
67
+ if digest and str(digest.get("sha256", "")).lower() == subject_sha256:
68
+ subject_match = True
69
+ break
70
+ if not subject_match:
71
+ issues.append("attested subject digest does not match the artifact")
72
+
73
+ predicate = _mapping(statement.get("predicate"))
74
+ build_definition = _mapping(predicate.get("buildDefinition")) if predicate else None
75
+ run_details = _mapping(predicate.get("runDetails")) if predicate else None
76
+ if build_definition is None or run_details is None:
77
+ return [*issues, "SLSA buildDefinition or runDetails is missing"]
78
+ if build_definition.get("buildType") != GITHUB_WORKFLOW_BUILD_TYPE:
79
+ issues.append("unexpected SLSA build type")
80
+
81
+ repo_url = f"https://github.com/{repository}"
82
+ external_parameters = _mapping(build_definition.get("externalParameters"))
83
+ if external_parameters is None or set(external_parameters) != {"workflow"}:
84
+ issues.append("unexpected SLSA external parameters")
85
+ workflow = _mapping(external_parameters.get("workflow")) if external_parameters else None
86
+ expected_workflow = {
87
+ "repository": repo_url,
88
+ "path": workflow_path,
89
+ "ref": source_ref,
90
+ }
91
+ if (
92
+ workflow is None
93
+ or set(workflow) != set(expected_workflow)
94
+ or any(workflow.get(key) != value for key, value in expected_workflow.items())
95
+ ):
96
+ issues.append("workflow repository/path/ref does not match release policy")
97
+
98
+ internal_parameters = _mapping(build_definition.get("internalParameters"))
99
+ github_parameters = _mapping(internal_parameters.get("github")) if internal_parameters else None
100
+ if github_parameters is None or github_parameters.get("event_name") != "push":
101
+ issues.append("attestation was not produced by a push event")
102
+ if github_parameters is None or github_parameters.get("runner_environment") != "github-hosted":
103
+ issues.append("attestation was not produced on a GitHub-hosted runner")
104
+
105
+ expected_uri = f"git+{repo_url}@{source_ref}"
106
+ dependency_match = False
107
+ dependencies = _sequence(build_definition.get("resolvedDependencies"))
108
+ if dependencies is not None:
109
+ for dependency in dependencies:
110
+ dependency_row = _mapping(dependency)
111
+ digest = _mapping(dependency_row.get("digest")) if dependency_row else None
112
+ if (
113
+ dependency_row
114
+ and dependency_row.get("uri") == expected_uri
115
+ and digest
116
+ and digest.get("gitCommit") == source_digest
117
+ ):
118
+ dependency_match = True
119
+ break
120
+ if not dependency_match:
121
+ issues.append("resolved source ref/digest does not match the release source")
122
+
123
+ builder = _mapping(run_details.get("builder"))
124
+ expected_builder = f"{repo_url}/{workflow_path}@{source_ref}"
125
+ if builder is None or builder.get("id") != expected_builder:
126
+ issues.append("builder identity does not match the release workflow")
127
+ return issues
128
+
129
+
130
+ def validate_verification_results(
131
+ payload: object,
132
+ *,
133
+ repository: str,
134
+ workflow_path: str,
135
+ source_ref: str,
136
+ source_digest: str,
137
+ subject_sha256: str,
138
+ ) -> list[str]:
139
+ """Return policy failures; one fully matching verified attestation is sufficient."""
140
+ rows = _sequence(payload)
141
+ if not rows:
142
+ return ["verification output contains no attestations"]
143
+ failures = [
144
+ _entry_issues(
145
+ entry,
146
+ repository=repository,
147
+ workflow_path=workflow_path,
148
+ source_ref=source_ref,
149
+ source_digest=source_digest,
150
+ subject_sha256=subject_sha256,
151
+ )
152
+ for entry in rows
153
+ ]
154
+ if any(not issues for issues in failures):
155
+ return []
156
+ return [
157
+ f"attestation[{index}]: {issue}"
158
+ for index, issues in enumerate(failures)
159
+ for issue in issues
160
+ ]
161
+
162
+
163
+ def _sha256(path: Path) -> str:
164
+ digest = hashlib.sha256()
165
+ with path.open("rb") as handle:
166
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
167
+ digest.update(chunk)
168
+ return digest.hexdigest()
169
+
170
+
171
+ def main(argv: Sequence[str] | None = None) -> int:
172
+ parser = argparse.ArgumentParser(description="enforce the ASH release-attestation policy")
173
+ parser.add_argument("--artifact", type=Path, required=True)
174
+ parser.add_argument("--verification-json", type=Path, required=True)
175
+ parser.add_argument("--repository", required=True)
176
+ parser.add_argument("--workflow-path", required=True)
177
+ parser.add_argument("--source-ref", required=True)
178
+ parser.add_argument("--source-digest", required=True)
179
+ args = parser.parse_args(argv)
180
+
181
+ try:
182
+ payload = json.loads(args.verification_json.read_text(encoding="utf-8"))
183
+ issues = validate_verification_results(
184
+ payload,
185
+ repository=args.repository,
186
+ workflow_path=args.workflow_path,
187
+ source_ref=args.source_ref,
188
+ source_digest=args.source_digest,
189
+ subject_sha256=_sha256(args.artifact),
190
+ )
191
+ except (OSError, json.JSONDecodeError, ValueError) as exc:
192
+ print(f"release attestation policy error: {type(exc).__name__}")
193
+ return 2
194
+ if issues:
195
+ for issue in issues:
196
+ print(f"release attestation policy failure: {issue}")
197
+ return 1
198
+ print(f"release attestation policy accepted: {args.artifact.name}")
199
+ return 0
200
+
201
+
202
+ if __name__ == "__main__":
203
+ raise SystemExit(main())