agents-shipgate 0.2.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 (47) hide show
  1. agents_shipgate/__init__.py +3 -0
  2. agents_shipgate/__main__.py +5 -0
  3. agents_shipgate/checks/__init__.py +2 -0
  4. agents_shipgate/checks/api.py +400 -0
  5. agents_shipgate/checks/auth.py +104 -0
  6. agents_shipgate/checks/base.py +71 -0
  7. agents_shipgate/checks/documentation.py +113 -0
  8. agents_shipgate/checks/inventory.py +70 -0
  9. agents_shipgate/checks/manifest_consistency.py +166 -0
  10. agents_shipgate/checks/manifest_scope.py +170 -0
  11. agents_shipgate/checks/policy.py +65 -0
  12. agents_shipgate/checks/registry.py +210 -0
  13. agents_shipgate/checks/schema.py +129 -0
  14. agents_shipgate/checks/side_effects.py +49 -0
  15. agents_shipgate/ci/__init__.py +2 -0
  16. agents_shipgate/ci/exit_policy.py +35 -0
  17. agents_shipgate/ci/github_summary.py +27 -0
  18. agents_shipgate/cli/__init__.py +2 -0
  19. agents_shipgate/cli/discovery.py +205 -0
  20. agents_shipgate/cli/main.py +477 -0
  21. agents_shipgate/cli/scan.py +366 -0
  22. agents_shipgate/config/__init__.py +2 -0
  23. agents_shipgate/config/loader.py +120 -0
  24. agents_shipgate/config/schema.py +312 -0
  25. agents_shipgate/core/__init__.py +2 -0
  26. agents_shipgate/core/baseline.py +113 -0
  27. agents_shipgate/core/context.py +16 -0
  28. agents_shipgate/core/errors.py +11 -0
  29. agents_shipgate/core/findings.py +249 -0
  30. agents_shipgate/core/logging.py +38 -0
  31. agents_shipgate/core/models.py +272 -0
  32. agents_shipgate/core/risk_hints.py +173 -0
  33. agents_shipgate/inputs/__init__.py +2 -0
  34. agents_shipgate/inputs/common.py +141 -0
  35. agents_shipgate/inputs/mcp.py +114 -0
  36. agents_shipgate/inputs/openai_api.py +355 -0
  37. agents_shipgate/inputs/openai_sdk_static.py +162 -0
  38. agents_shipgate/inputs/openapi.py +324 -0
  39. agents_shipgate/py.typed +1 -0
  40. agents_shipgate/report/__init__.py +2 -0
  41. agents_shipgate/report/json_report.py +10 -0
  42. agents_shipgate/report/markdown.py +248 -0
  43. agents_shipgate-0.2.0.dist-info/METADATA +217 -0
  44. agents_shipgate-0.2.0.dist-info/RECORD +47 -0
  45. agents_shipgate-0.2.0.dist-info/WHEEL +4 -0
  46. agents_shipgate-0.2.0.dist-info/entry_points.txt +12 -0
  47. agents_shipgate-0.2.0.dist-info/licenses/LICENSE +185 -0
@@ -0,0 +1,3 @@
1
+ """Agents Shipgate package."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,5 @@
1
+ from agents_shipgate.cli.main import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
5
+
@@ -0,0 +1,2 @@
1
+ """Deterministic Agents Shipgate checks."""
2
+
@@ -0,0 +1,400 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from agents_shipgate.checks.base import agent_finding, tool_finding
6
+ from agents_shipgate.core.context import ScanContext
7
+ from agents_shipgate.core.models import Tool, ToolParameter
8
+ from agents_shipgate.core.risk_hints import (
9
+ has_risk_tag,
10
+ is_high_risk_tool,
11
+ is_write_tool,
12
+ risk_tags,
13
+ )
14
+
15
+ BROAD_TEXT_NAMES = {
16
+ "action",
17
+ "body",
18
+ "command",
19
+ "content",
20
+ "instructions",
21
+ "message",
22
+ "prompt",
23
+ "update",
24
+ "updates",
25
+ }
26
+ RISKY_NUMERIC_NAMES = {"amount", "amt", "count", "qty", "quantity", "limit", "cap", "size"}
27
+ READ_ONLY_PROMPT_TERMS = (
28
+ "advise only",
29
+ "advice only",
30
+ "only advise",
31
+ "read-only",
32
+ "read only",
33
+ "do not take action",
34
+ )
35
+ APPROVAL_PROMPT_TERMS = ("approval", "approved", "human review", "requires review")
36
+ CONFIRMATION_PROMPT_TERMS = ("confirm", "confirmation", "explicit consent", "ask before")
37
+
38
+
39
+ def run(context: ScanContext):
40
+ if context.api_artifacts is None:
41
+ return []
42
+ findings = []
43
+ findings.extend(_function_schema_strictness(context))
44
+ findings.extend(_structured_output_readiness(context))
45
+ findings.extend(_prompt_tool_scope_mismatch(context))
46
+ findings.extend(_operational_readiness(context))
47
+ return findings
48
+
49
+
50
+ def _function_schema_strictness(context: ScanContext):
51
+ findings = []
52
+ for tool in _api_tools(context):
53
+ issues = _function_schema_issues(tool)
54
+ if not issues:
55
+ continue
56
+ high_risk = is_write_tool(tool) or is_high_risk_tool(tool)
57
+ findings.append(
58
+ tool_finding(
59
+ tool=tool,
60
+ check_id="SHIP-API-FUNCTION-SCHEMA-STRICTNESS",
61
+ title=f"{tool.name} function schema is not strict enough",
62
+ severity="high" if high_risk else "medium",
63
+ category="api",
64
+ evidence={"issues": issues, "risk_tags": risk_tags(tool, min_confidence="medium")},
65
+ confidence="high",
66
+ recommendation=(
67
+ f"Make {tool.name} a strict function schema: object parameters, "
68
+ "additionalProperties=false, complete required list, and bounded risky fields."
69
+ ),
70
+ context=context,
71
+ )
72
+ )
73
+ return findings
74
+
75
+
76
+ def _structured_output_readiness(context: ScanContext):
77
+ artifacts = context.api_artifacts
78
+ if artifacts is None:
79
+ return []
80
+ high_risk_tools = [tool.name for tool in _api_tools(context) if is_high_risk_tool(tool)]
81
+ if not artifacts.response_formats:
82
+ return [
83
+ agent_finding(
84
+ check_id="SHIP-API-STRUCTURED-OUTPUT-READINESS",
85
+ title="OpenAI API response format is not declared",
86
+ severity="high" if high_risk_tools else "medium",
87
+ category="api",
88
+ evidence={"high_risk_tools": high_risk_tools},
89
+ confidence="high",
90
+ recommendation=(
91
+ "Declare a structured response format with decision/status, error/refusal, "
92
+ "and needs_review fields where downstream behavior depends on the output."
93
+ ),
94
+ context=context,
95
+ )
96
+ ]
97
+
98
+ findings = []
99
+ for response_format in artifacts.response_formats:
100
+ issues = _response_schema_issues(
101
+ response_format.json_schema,
102
+ response_format.downstream_critical_fields,
103
+ )
104
+ if not issues:
105
+ continue
106
+ findings.append(
107
+ agent_finding(
108
+ check_id="SHIP-API-STRUCTURED-OUTPUT-READINESS",
109
+ title=f"Response format {response_format.path} is under-specified",
110
+ severity="medium",
111
+ category="api",
112
+ evidence={
113
+ "path": response_format.path,
114
+ "issues": issues,
115
+ "downstream_critical_fields": response_format.downstream_critical_fields,
116
+ },
117
+ confidence="medium",
118
+ recommendation=(
119
+ "Tighten the structured output schema with enums, "
120
+ "needs_review/refusal/error modeling, and declared critical fields."
121
+ ),
122
+ context=context,
123
+ )
124
+ )
125
+ return findings
126
+
127
+
128
+ def _prompt_tool_scope_mismatch(context: ScanContext):
129
+ artifacts = context.api_artifacts
130
+ if artifacts is None or not artifacts.prompt_text:
131
+ return []
132
+ prompt = artifacts.prompt_text.lower()
133
+ api_tools = _api_tools(context)
134
+ write_or_high_risk = [
135
+ tool for tool in api_tools if is_write_tool(tool) or is_high_risk_tool(tool)
136
+ ]
137
+ findings = []
138
+ if write_or_high_risk and any(term in prompt for term in READ_ONLY_PROMPT_TERMS):
139
+ findings.append(
140
+ agent_finding(
141
+ check_id="SHIP-API-PROMPT-TOOL-SCOPE-MISMATCH",
142
+ title=(
143
+ "Prompt says read-only or advise-only while write/high-risk "
144
+ "tools are enabled"
145
+ ),
146
+ severity="high",
147
+ category="api",
148
+ evidence={"tools": [tool.name for tool in write_or_high_risk]},
149
+ confidence="high",
150
+ recommendation=(
151
+ "Align prompt scope with enabled tools or remove write/high-risk tools."
152
+ ),
153
+ context=context,
154
+ )
155
+ )
156
+ needs_confirmation = [
157
+ tool
158
+ for tool in api_tools
159
+ if has_risk_tag(
160
+ tool,
161
+ {"destructive", "external_write", "customer_communication", "financial_action"},
162
+ min_confidence="medium",
163
+ )
164
+ ]
165
+ if needs_confirmation and not (
166
+ any(term in prompt for term in CONFIRMATION_PROMPT_TERMS)
167
+ and any(term in prompt for term in APPROVAL_PROMPT_TERMS)
168
+ ):
169
+ findings.append(
170
+ agent_finding(
171
+ check_id="SHIP-API-PROMPT-TOOL-SCOPE-MISMATCH",
172
+ title="Prompt lacks approval/confirmation language for high-risk tools",
173
+ severity="medium",
174
+ category="api",
175
+ evidence={"tools": [tool.name for tool in needs_confirmation]},
176
+ confidence="medium",
177
+ recommendation=(
178
+ "Add prompt instructions requiring human approval and explicit confirmation "
179
+ "before financial, destructive, or external customer actions."
180
+ ),
181
+ context=context,
182
+ )
183
+ )
184
+ return findings
185
+
186
+
187
+ def _operational_readiness(context: ScanContext):
188
+ artifacts = context.api_artifacts
189
+ if artifacts is None:
190
+ return []
191
+ findings = []
192
+ api_tools = _api_tools(context)
193
+ high_risk_tools = [tool for tool in api_tools if is_high_risk_tool(tool)]
194
+ retry_policy = artifacts.retry_policy()
195
+ timeouts = artifacts.timeouts()
196
+ output_schemas = artifacts.tool_output_schemas()
197
+
198
+ if high_risk_tools and not retry_policy:
199
+ findings.append(
200
+ agent_finding(
201
+ check_id="SHIP-API-OPERATIONAL-READINESS",
202
+ title="OpenAI API flow lacks retry policy metadata",
203
+ severity="medium",
204
+ category="api",
205
+ evidence={"high_risk_tools": [tool.name for tool in high_risk_tools]},
206
+ confidence="medium",
207
+ recommendation="Declare retry_policy in openai_api.policy_rules or model_config.",
208
+ context=context,
209
+ )
210
+ )
211
+ if high_risk_tools and not timeouts:
212
+ findings.append(
213
+ agent_finding(
214
+ check_id="SHIP-API-OPERATIONAL-READINESS",
215
+ title="OpenAI API flow lacks timeout metadata",
216
+ severity="medium",
217
+ category="api",
218
+ evidence={"high_risk_tools": [tool.name for tool in high_risk_tools]},
219
+ confidence="medium",
220
+ recommendation="Declare tool-call timeout metadata for high-risk OpenAI API flows.",
221
+ context=context,
222
+ )
223
+ )
224
+ if high_risk_tools and not artifacts.test_cases:
225
+ findings.append(
226
+ agent_finding(
227
+ check_id="SHIP-API-OPERATIONAL-READINESS",
228
+ title="OpenAI API flow lacks test case metadata for high-risk tools",
229
+ severity="medium",
230
+ category="api",
231
+ evidence={"high_risk_tools": [tool.name for tool in high_risk_tools]},
232
+ confidence="medium",
233
+ recommendation="Add simple OpenAI API test cases for high-risk tool-call flows.",
234
+ context=context,
235
+ )
236
+ )
237
+ for tool in high_risk_tools:
238
+ if tool.name not in output_schemas:
239
+ findings.append(
240
+ tool_finding(
241
+ tool=tool,
242
+ check_id="SHIP-API-OPERATIONAL-READINESS",
243
+ title=f"{tool.name} lacks success/failure output modeling",
244
+ severity="medium",
245
+ category="api",
246
+ evidence={"tool_output_schemas": sorted(output_schemas)},
247
+ confidence="medium",
248
+ recommendation=(
249
+ f"Declare success_fields and failure_fields for {tool.name} "
250
+ "in openai_api policy rules."
251
+ ),
252
+ context=context,
253
+ )
254
+ )
255
+ if retry_policy and _needs_idempotency(tool, artifacts):
256
+ findings.append(
257
+ tool_finding(
258
+ tool=tool,
259
+ check_id="SHIP-API-OPERATIONAL-READINESS",
260
+ title=f"{tool.name} may be retried without idempotency evidence",
261
+ severity="high",
262
+ category="api",
263
+ evidence={
264
+ "retry_policy": retry_policy,
265
+ "risk_tags": risk_tags(tool, min_confidence="medium"),
266
+ },
267
+ confidence="high",
268
+ recommendation=(
269
+ f"Add idempotency evidence for {tool.name} or avoid retrying "
270
+ "this side effect."
271
+ ),
272
+ context=context,
273
+ )
274
+ )
275
+ _append_trace_findings(findings, context)
276
+ return findings
277
+
278
+
279
+ def _append_trace_findings(findings: list, context: ScanContext) -> None:
280
+ artifacts = context.api_artifacts
281
+ if artifacts is None:
282
+ return
283
+ approval_tools = context.manifest.policies.approval_tools() | artifacts.approval_tools()
284
+ confirmation_tools = (
285
+ context.manifest.policies.confirmation_tools() | artifacts.confirmation_tools()
286
+ )
287
+ for event in artifacts.trace_samples:
288
+ tool_name = event.get("tool_name")
289
+ if not isinstance(tool_name, str):
290
+ continue
291
+ if tool_name in approval_tools and event.get("approved") is False:
292
+ findings.append(
293
+ agent_finding(
294
+ check_id="SHIP-API-OPERATIONAL-READINESS",
295
+ title=f"Trace sample shows {tool_name} without approval",
296
+ severity="medium",
297
+ category="api",
298
+ evidence={"tool_name": tool_name, "approved": event.get("approved")},
299
+ confidence="medium",
300
+ recommendation=f"Require approval before calling {tool_name}.",
301
+ context=context,
302
+ )
303
+ )
304
+ if tool_name in confirmation_tools and event.get("confirmed") is False:
305
+ findings.append(
306
+ agent_finding(
307
+ check_id="SHIP-API-OPERATIONAL-READINESS",
308
+ title=f"Trace sample shows {tool_name} without confirmation",
309
+ severity="medium",
310
+ category="api",
311
+ evidence={"tool_name": tool_name, "confirmed": event.get("confirmed")},
312
+ confidence="medium",
313
+ recommendation=f"Require explicit confirmation before calling {tool_name}.",
314
+ context=context,
315
+ )
316
+ )
317
+
318
+
319
+ def _function_schema_issues(tool: Tool) -> list[str]:
320
+ issues: list[str] = []
321
+ schema = tool.input_schema
322
+ if not schema:
323
+ return ["missing_parameters_schema"]
324
+ if tool.annotations.get("openaiStrict") is not True:
325
+ issues.append("missing_strict_true")
326
+ if schema.get("type") != "object":
327
+ issues.append("parameters_schema_not_object")
328
+ if schema.get("additionalProperties") is not False:
329
+ issues.append("additional_properties_not_false")
330
+ properties = schema.get("properties")
331
+ if isinstance(properties, dict):
332
+ required = set(schema.get("required") or [])
333
+ missing_required = sorted(set(properties) - required)
334
+ if missing_required:
335
+ issues.append(f"properties_missing_from_required:{','.join(missing_required)}")
336
+ for parameter in tool.parameters:
337
+ if _risky_field_without_bounds_or_enum(parameter):
338
+ issues.append(f"risky_field_unbounded:{parameter.name}")
339
+ if _broad_free_text(parameter):
340
+ issues.append(f"broad_free_text:{parameter.name}")
341
+ return issues
342
+
343
+
344
+ def _response_schema_issues(schema: dict[str, Any], critical_fields: list[str]) -> list[str]:
345
+ issues: list[str] = []
346
+ if schema.get("type") != "object":
347
+ issues.append("response_schema_not_object")
348
+ if schema.get("additionalProperties") is not False:
349
+ issues.append("additional_properties_not_false")
350
+ properties = schema.get("properties") if isinstance(schema.get("properties"), dict) else {}
351
+ if not critical_fields:
352
+ issues.append("missing_downstream_critical_fields")
353
+ else:
354
+ missing_critical = sorted(set(critical_fields) - set(properties))
355
+ if missing_critical:
356
+ issues.append(f"critical_fields_missing_from_schema:{','.join(missing_critical)}")
357
+ if not any(field in properties for field in ("refusal", "needs_review", "error")):
358
+ issues.append("missing_refusal_needs_review_or_error_field")
359
+ for field in ("decision", "status"):
360
+ value = properties.get(field)
361
+ if isinstance(value, dict) and not value.get("enum"):
362
+ issues.append(f"missing_enum:{field}")
363
+ return issues
364
+
365
+
366
+ def _needs_idempotency(tool: Tool, artifacts) -> bool:
367
+ if tool.name in artifacts.idempotency_tools():
368
+ return False
369
+ if tool.annotations.get("idempotentHint") is True:
370
+ return False
371
+ if any(parameter.name == "idempotency_key" for parameter in tool.parameters):
372
+ return False
373
+ return is_write_tool(tool) and has_risk_tag(
374
+ tool,
375
+ {"financial_action", "destructive", "external_write"},
376
+ min_confidence="medium",
377
+ )
378
+
379
+
380
+ def _risky_field_without_bounds_or_enum(parameter: ToolParameter) -> bool:
381
+ name = parameter.name.lower()
382
+ risky_name = any(token in name for token in RISKY_NUMERIC_NAMES)
383
+ return (
384
+ risky_name
385
+ and parameter.type in {"number", "integer"}
386
+ and parameter.maximum is None
387
+ and not parameter.enum
388
+ )
389
+
390
+
391
+ def _broad_free_text(parameter: ToolParameter) -> bool:
392
+ return (
393
+ parameter.name.lower() in BROAD_TEXT_NAMES
394
+ and parameter.type in {None, "string", "object"}
395
+ and not parameter.enum
396
+ )
397
+
398
+
399
+ def _api_tools(context: ScanContext) -> list[Tool]:
400
+ return [tool for tool in context.tools if tool.source_type == "openai_api"]
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ from agents_shipgate.checks.base import agent_finding, tool_finding
4
+ from agents_shipgate.core.context import ScanContext
5
+ from agents_shipgate.core.risk_hints import has_risk_tag, is_write_tool
6
+
7
+
8
+ def run(context: ScanContext):
9
+ findings = []
10
+ broad_global_scopes = [scope for scope in context.manifest.permissions.scopes if _is_broad_scope(scope)]
11
+ if broad_global_scopes:
12
+ findings.append(
13
+ agent_finding(
14
+ check_id="SHIP-AUTH-MANIFEST-BROAD-SCOPE",
15
+ title="Manifest declares broad permission scopes",
16
+ severity="high",
17
+ category="auth",
18
+ evidence={"scopes": broad_global_scopes},
19
+ confidence="high",
20
+ recommendation="Replace broad manifest permission scopes with the narrowest scopes needed for this release.",
21
+ context=context,
22
+ )
23
+ )
24
+ for tool in context.tools:
25
+ if _tool_requires_scope(tool) and not tool.auth.scopes:
26
+ findings.append(
27
+ tool_finding(
28
+ tool=tool,
29
+ check_id="SHIP-AUTH-MISSING-SCOPE",
30
+ title=f"{tool.name} lacks declared auth scopes",
31
+ severity="high",
32
+ category="auth",
33
+ evidence={"risk_tags": [hint.tag for hint in tool.risk_hints if hint.confidence in {"medium", "high"}]},
34
+ confidence="medium",
35
+ recommendation=f"Declare auth scopes for {tool.name} in OpenAPI, MCP metadata, or the manifest before release review.",
36
+ context=context,
37
+ )
38
+ )
39
+ missing_scopes = [
40
+ scope
41
+ for scope in tool.auth.scopes
42
+ if not _scope_covered(scope, context.manifest.permissions.scopes)
43
+ ]
44
+ if missing_scopes:
45
+ findings.append(
46
+ tool_finding(
47
+ tool=tool,
48
+ check_id="SHIP-AUTH-SCOPE-COVERAGE-MISSING",
49
+ title=f"{tool.name} requires scopes not declared in the manifest",
50
+ severity="high",
51
+ category="auth",
52
+ evidence={
53
+ "tool_scopes": tool.auth.scopes,
54
+ "manifest_scopes": context.manifest.permissions.scopes,
55
+ "missing_scopes": missing_scopes,
56
+ },
57
+ confidence="high",
58
+ recommendation=(
59
+ f"Add the required scopes for {tool.name} to permissions.scopes "
60
+ "or narrow the tool's declared auth requirements."
61
+ ),
62
+ context=context,
63
+ )
64
+ )
65
+ broad_scopes = [scope for scope in tool.auth.scopes if _is_broad_scope(scope)]
66
+ if broad_scopes:
67
+ findings.append(
68
+ tool_finding(
69
+ tool=tool,
70
+ check_id="SHIP-AUTH-TOOL-BROAD-SCOPE",
71
+ title=f"{tool.name} uses broad auth scopes",
72
+ severity="high",
73
+ category="auth",
74
+ evidence={"scopes": broad_scopes},
75
+ confidence="high",
76
+ recommendation=f"Replace broad scopes for {tool.name} with narrower operation-specific scopes.",
77
+ context=context,
78
+ )
79
+ )
80
+ return findings
81
+
82
+
83
+ def _is_broad_scope(scope: str) -> bool:
84
+ lowered = scope.lower()
85
+ return lowered in {"*", "admin"} or lowered.endswith(":*") or "write-all" in lowered or "admin" in lowered
86
+
87
+
88
+ def _tool_requires_scope(tool) -> bool:
89
+ return is_write_tool(tool) or has_risk_tag(
90
+ tool,
91
+ {"sensitive_data_access"},
92
+ min_confidence="medium",
93
+ )
94
+
95
+
96
+ def _scope_covered(required_scope: str, manifest_scopes: list[str]) -> bool:
97
+ required = required_scope.lower()
98
+ for declared_scope in manifest_scopes:
99
+ declared = declared_scope.lower()
100
+ if declared in {"*", required}:
101
+ return True
102
+ if declared.endswith(":*") and required.startswith(declared[:-1]):
103
+ return True
104
+ return False
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from agents_shipgate.core.context import ScanContext
6
+ from agents_shipgate.core.models import (
7
+ Finding,
8
+ SourceReference,
9
+ Tool,
10
+ parse_confidence,
11
+ parse_severity,
12
+ )
13
+
14
+
15
+ def tool_finding(
16
+ *,
17
+ tool: Tool,
18
+ check_id: str,
19
+ title: str,
20
+ severity: str,
21
+ category: str,
22
+ evidence: dict[str, object],
23
+ confidence: str,
24
+ recommendation: str,
25
+ context: ScanContext,
26
+ ) -> Finding:
27
+ return Finding(
28
+ check_id=check_id,
29
+ title=title,
30
+ severity=parse_severity(severity),
31
+ category=category,
32
+ tool_id=tool.id,
33
+ tool_name=tool.name,
34
+ agent_id=context.agent.id,
35
+ evidence=evidence,
36
+ confidence=parse_confidence(confidence),
37
+ source=SourceReference(
38
+ type=tool.source_type,
39
+ ref=tool.source_ref,
40
+ location=tool.source_location,
41
+ ),
42
+ recommendation=recommendation,
43
+ )
44
+
45
+
46
+ def agent_finding(
47
+ *,
48
+ check_id: str,
49
+ title: str,
50
+ severity: str,
51
+ category: str,
52
+ evidence: dict[str, object],
53
+ confidence: str,
54
+ recommendation: str,
55
+ context: ScanContext,
56
+ ) -> Finding:
57
+ return Finding(
58
+ check_id=check_id,
59
+ title=title,
60
+ severity=parse_severity(severity),
61
+ category=category,
62
+ agent_id=context.agent.id,
63
+ evidence=evidence,
64
+ confidence=parse_confidence(confidence),
65
+ source=SourceReference(type="manifest", ref=_manifest_ref(context.config_path)),
66
+ recommendation=recommendation,
67
+ )
68
+
69
+
70
+ def _manifest_ref(config_path: Path) -> str:
71
+ return config_path.name