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.
- agentic_security_harness/__init__.py +220 -0
- agentic_security_harness/adapter_base.py +61 -0
- agentic_security_harness/adapters.py +197 -0
- agentic_security_harness/attestation_policy.py +203 -0
- agentic_security_harness/cli.py +4752 -0
- agentic_security_harness/companion_contracts.py +1272 -0
- agentic_security_harness/context_consent_campaign.py +730 -0
- agentic_security_harness/corpus.py +558 -0
- agentic_security_harness/covering_array.py +145 -0
- agentic_security_harness/demo_adapter.py +719 -0
- agentic_security_harness/demo_agent.py +551 -0
- agentic_security_harness/doctor.py +306 -0
- agentic_security_harness/envelope_policy.py +145 -0
- agentic_security_harness/evidence_campaign.py +918 -0
- agentic_security_harness/evidence_quality.py +1141 -0
- agentic_security_harness/evidence_status.py +383 -0
- agentic_security_harness/external_openai_compatible.py +220 -0
- agentic_security_harness/external_prompt.py +88 -0
- agentic_security_harness/external_runner.py +958 -0
- agentic_security_harness/external_validation.py +121 -0
- agentic_security_harness/ground_truth.py +116 -0
- agentic_security_harness/handoff_integrity.py +351 -0
- agentic_security_harness/html_report.py +620 -0
- agentic_security_harness/local_profiles.py +180 -0
- agentic_security_harness/local_swarm.py +1142 -0
- agentic_security_harness/local_swarm_matrix.py +1396 -0
- agentic_security_harness/marketing_web_injection_campaign.py +918 -0
- agentic_security_harness/marketing_web_live_campaign.py +1186 -0
- agentic_security_harness/matrix.py +407 -0
- agentic_security_harness/memory_governance.py +136 -0
- agentic_security_harness/memory_rehydration_campaign.py +1033 -0
- agentic_security_harness/mock_target.py +190 -0
- agentic_security_harness/models.py +237 -0
- agentic_security_harness/patterns.py +695 -0
- agentic_security_harness/planner_task_campaign.py +1033 -0
- agentic_security_harness/portfolio_contract.py +552 -0
- agentic_security_harness/presets.py +298 -0
- agentic_security_harness/protected_demo_agent.py +484 -0
- agentic_security_harness/py.typed +1 -0
- agentic_security_harness/r5_public_projection.py +633 -0
- agentic_security_harness/rag_context_campaign.py +1020 -0
- agentic_security_harness/reconciliation.py +869 -0
- agentic_security_harness/remediation.py +808 -0
- agentic_security_harness/reporting.py +276 -0
- agentic_security_harness/reviewer_receipt.py +112 -0
- agentic_security_harness/run_config.py +278 -0
- agentic_security_harness/run_diff.py +487 -0
- agentic_security_harness/run_manifest.py +336 -0
- agentic_security_harness/rundb.py +320 -0
- agentic_security_harness/runner.py +133 -0
- agentic_security_harness/runtime_guard_foundation.py +1203 -0
- agentic_security_harness/safe_io.py +442 -0
- agentic_security_harness/safe_markdown.py +78 -0
- agentic_security_harness/safe_terminal.py +46 -0
- agentic_security_harness/scenario_adjudication.py +285 -0
- agentic_security_harness/scenario_timeline.py +297 -0
- agentic_security_harness/scenarios.py +528 -0
- agentic_security_harness/schema_versions.py +156 -0
- agentic_security_harness/scorecard.py +46 -0
- agentic_security_harness/secret_hygiene.py +126 -0
- agentic_security_harness/secret_leak_campaign.py +1456 -0
- agentic_security_harness/semantic_drift_campaign.py +913 -0
- agentic_security_harness/semantic_propagation_campaign.py +1229 -0
- agentic_security_harness/showcase.py +766 -0
- agentic_security_harness/source_identity.py +44 -0
- agentic_security_harness/standards_mapping.py +254 -0
- agentic_security_harness/static_import_closure.py +509 -0
- agentic_security_harness/stats.py +493 -0
- agentic_security_harness/swarm_defense_contour.py +585 -0
- agentic_security_harness/swarm_defense_live_campaign.py +1258 -0
- agentic_security_harness/swarm_resilience_campaign.py +983 -0
- agentic_security_harness/tool_authority_campaign.py +890 -0
- agentic_security_harness/toy_adapters.py +472 -0
- agentic_security_harness/trace_schema.py +49 -0
- agentic_security_harness/trading_bot_stand.py +4866 -0
- agentic_security_harness/validation.py +6796 -0
- agentic_security_harness/version.py +3 -0
- agentic_security_harness-1.0.0.dist-info/METADATA +286 -0
- agentic_security_harness-1.0.0.dist-info/RECORD +84 -0
- agentic_security_harness-1.0.0.dist-info/WHEEL +5 -0
- agentic_security_harness-1.0.0.dist-info/entry_points.txt +2 -0
- agentic_security_harness-1.0.0.dist-info/licenses/LICENSE +201 -0
- agentic_security_harness-1.0.0.dist-info/licenses/NOTICE +10 -0
- 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())
|