switch-trust-cli 1.7.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 (157) hide show
  1. switch_trust/__init__.py +0 -0
  2. switch_trust/cli/__init__.py +0 -0
  3. switch_trust/cli/__main__.py +3 -0
  4. switch_trust/cli/builtin_config.json +879 -0
  5. switch_trust/cli/console.py +127 -0
  6. switch_trust/cli/eval_cli.py +905 -0
  7. switch_trust/cli/file_filter.py +172 -0
  8. switch_trust/cli/init_cli.py +156 -0
  9. switch_trust/cli/main.py +384 -0
  10. switch_trust/cli/output_formatters.py +482 -0
  11. switch_trust/cli/rich_observer.py +89 -0
  12. switch_trust/cli/runner.py +274 -0
  13. switch_trust/cli/scan_cli.py +314 -0
  14. switch_trust/cli/telemetry.py +430 -0
  15. switch_trust/cli/utils.py +158 -0
  16. switch_trust/cli/version.py +1 -0
  17. switch_trust/data/detector_prompts/llm01_adversarial.txt +59 -0
  18. switch_trust/data/detector_prompts/llm01_fixed.txt +6 -0
  19. switch_trust/data/detector_prompts/llm02_adversarial.txt +79 -0
  20. switch_trust/data/detector_prompts/llm02_fixed.txt +6 -0
  21. switch_trust/data/detector_prompts/llm05_adversarial.txt +67 -0
  22. switch_trust/data/detector_prompts/llm05_fixed.txt +14 -0
  23. switch_trust/data/detector_prompts/llm06_adversarial.txt +139 -0
  24. switch_trust/data/detector_prompts/llm06_fixed.txt +13 -0
  25. switch_trust/data/detector_prompts/llm07_adversarial.txt +189 -0
  26. switch_trust/data/detector_prompts/llm07_fixed.txt +12 -0
  27. switch_trust/data/detector_prompts/llm09_adversarial.txt +83 -0
  28. switch_trust/data/detector_prompts/llm09_fixed.txt +13 -0
  29. switch_trust/data/llm01_prompt_injection.csv +6708 -0
  30. switch_trust/data/llm02_sensitive_information.csv +1069 -0
  31. switch_trust/data/llm05_unsafe_output.csv +3882 -0
  32. switch_trust/data/llm06_excessive_agency.csv +778 -0
  33. switch_trust/data/llm07_system_prompt_leakage.csv +971 -0
  34. switch_trust/data/llm09_hallucination-goals.csv +32419 -0
  35. switch_trust/data/llm09_hallucination.csv +599 -0
  36. switch_trust/data/pii_leakage.csv +706 -0
  37. switch_trust/data/secret_leakage.csv +1380 -0
  38. switch_trust/eval/__init__.py +0 -0
  39. switch_trust/eval/common/__init__.py +0 -0
  40. switch_trust/eval/common/converter_anthropic.py +172 -0
  41. switch_trust/eval/common/converter_genai.py +171 -0
  42. switch_trust/eval/common/converter_openai.py +198 -0
  43. switch_trust/eval/common/log.py +116 -0
  44. switch_trust/eval/common/reference.py +25 -0
  45. switch_trust/eval/common/schema.py +190 -0
  46. switch_trust/eval/common/utils.py +119 -0
  47. switch_trust/eval/core/__init__.py +0 -0
  48. switch_trust/eval/core/detectors/__init__.py +0 -0
  49. switch_trust/eval/core/detectors/detector.py +25 -0
  50. switch_trust/eval/core/detectors/detector_garak.py +112 -0
  51. switch_trust/eval/core/detectors/detector_model.py +123 -0
  52. switch_trust/eval/core/detectors/detector_model_adversarial.py +91 -0
  53. switch_trust/eval/core/detectors/detector_pii.py +67 -0
  54. switch_trust/eval/core/detectors/detector_secret.py +88 -0
  55. switch_trust/eval/core/detectors/detector_topic_guard.py +63 -0
  56. switch_trust/eval/core/detectors/detector_toxicity.py +78 -0
  57. switch_trust/eval/core/eval/__init__.py +0 -0
  58. switch_trust/eval/core/eval/eval_creator.py +166 -0
  59. switch_trust/eval/core/eval/evaluation.py +112 -0
  60. switch_trust/eval/core/eval/evaluation_adversarial.py +412 -0
  61. switch_trust/eval/core/eval/evaluation_garak_module.py +45 -0
  62. switch_trust/eval/core/eval/evaluation_garak_probe.py +308 -0
  63. switch_trust/eval/core/eval/evaluation_message_list.py +46 -0
  64. switch_trust/eval/core/eval/evaluation_multi.py +276 -0
  65. switch_trust/eval/core/eval/evaluation_single.py +89 -0
  66. switch_trust/eval/core/eval/evaluation_single_prompt.py +62 -0
  67. switch_trust/eval/core/eval/evaluation_topic_guard.py +305 -0
  68. switch_trust/eval/core/eval/metric_conciseness.py +179 -0
  69. switch_trust/eval/core/eval/metric_factual_accuracy.py +312 -0
  70. switch_trust/eval/core/eval/metric_instruction_adherence.py +177 -0
  71. switch_trust/eval/core/eval/metric_tone.py +180 -0
  72. switch_trust/eval/core/eval/metric_toxicity.py +146 -0
  73. switch_trust/eval/core/eval/observer.py +137 -0
  74. switch_trust/eval/core/message/__init__.py +0 -0
  75. switch_trust/eval/core/message/message_collection.py +37 -0
  76. switch_trust/eval/core/message/message_collection_csv.py +44 -0
  77. switch_trust/eval/core/message/message_collection_garak.py +46 -0
  78. switch_trust/eval/core/message/message_collection_memory.py +34 -0
  79. switch_trust/eval/core/models/__init__.py +0 -0
  80. switch_trust/eval/core/models/generator_model.py +421 -0
  81. switch_trust/eval/core/models/model.py +172 -0
  82. switch_trust/eval/core/models/model_adk.py +186 -0
  83. switch_trust/eval/core/models/model_anthropic.py +79 -0
  84. switch_trust/eval/core/models/model_anthropic_agent.py +104 -0
  85. switch_trust/eval/core/models/model_gemini.py +146 -0
  86. switch_trust/eval/core/models/model_generic_http.py +120 -0
  87. switch_trust/eval/core/models/model_http.py +135 -0
  88. switch_trust/eval/core/models/model_huggingface.py +75 -0
  89. switch_trust/eval/core/models/model_langserve.py +96 -0
  90. switch_trust/eval/core/models/model_litellm.py +52 -0
  91. switch_trust/eval/core/models/model_ollama.py +65 -0
  92. switch_trust/eval/core/models/model_openai.py +71 -0
  93. switch_trust/eval/core/models/model_openai_agent.py +92 -0
  94. switch_trust/eval/core/models/model_openai_compatible.py +66 -0
  95. switch_trust/eval/core/models/model_per_endpoint_limiter.py +80 -0
  96. switch_trust/eval/core/models/model_retry.py +204 -0
  97. switch_trust/eval/core/models/model_sync_wrapper.py +38 -0
  98. switch_trust/eval/core/models/model_vertex_agent_runtime.py +315 -0
  99. switch_trust/eval/core/models/response_schema.py +148 -0
  100. switch_trust/eval/core/optional_deps.py +57 -0
  101. switch_trust/eval/db/__init__.py +0 -0
  102. switch_trust/eval/db/base/__init__.py +0 -0
  103. switch_trust/eval/db/base/common_types.py +16 -0
  104. switch_trust/eval/db/base/detectors/__init__.py +0 -0
  105. switch_trust/eval/db/base/detectors/detector_helpers.py +88 -0
  106. switch_trust/eval/db/base/detectors/detector_repository.py +96 -0
  107. switch_trust/eval/db/base/detectors/detector_types.py +77 -0
  108. switch_trust/eval/db/base/eval/__init__.py +0 -0
  109. switch_trust/eval/db/base/eval/eval_helpers.py +186 -0
  110. switch_trust/eval/db/base/eval/eval_repository.py +120 -0
  111. switch_trust/eval/db/base/eval/eval_run.py +142 -0
  112. switch_trust/eval/db/base/eval/eval_types.py +127 -0
  113. switch_trust/eval/db/base/eval/model_eval_repository.py +79 -0
  114. switch_trust/eval/db/base/eval/model_eval_run_repository.py +72 -0
  115. switch_trust/eval/db/base/eval/model_eval_run_result_repository.py +42 -0
  116. switch_trust/eval/db/base/eval/model_eval_run_result_types.py +21 -0
  117. switch_trust/eval/db/base/eval/model_eval_run_types.py +78 -0
  118. switch_trust/eval/db/base/eval/model_eval_types.py +69 -0
  119. switch_trust/eval/db/base/message/__init__.py +0 -0
  120. switch_trust/eval/db/base/message/message_collection_helpers.py +50 -0
  121. switch_trust/eval/db/base/message/message_collection_repository.py +95 -0
  122. switch_trust/eval/db/base/message/message_collection_types.py +87 -0
  123. switch_trust/eval/db/base/models/__init__.py +0 -0
  124. switch_trust/eval/db/base/models/model_helpers.py +253 -0
  125. switch_trust/eval/db/base/models/model_repository.py +104 -0
  126. switch_trust/eval/db/base/models/model_types.py +159 -0
  127. switch_trust/eval/db/json/__init__.py +0 -0
  128. switch_trust/eval/db/json/repository_json.py +559 -0
  129. switch_trust/scan/__init__.py +12 -0
  130. switch_trust/scan/agent_scanner.py +812 -0
  131. switch_trust/scan/config/agent_opengrep_rules.yaml +533 -0
  132. switch_trust/scan/config/agent_taxonomy.json +517 -0
  133. switch_trust/scan/config/agentic_cvss_mapping.yaml +136 -0
  134. switch_trust/scan/config/compliance_mappings.json +110 -0
  135. switch_trust/scan/config/triage_prompt.txt +326 -0
  136. switch_trust/scan/constants.py +15 -0
  137. switch_trust/scan/exception_classification.py +179 -0
  138. switch_trust/scan/llm_provider.py +330 -0
  139. switch_trust/scan/opengrep_resolver.py +12 -0
  140. switch_trust/scan/reasoner.py +602 -0
  141. switch_trust/scan/schema.py +315 -0
  142. switch_trust/scan/scorer.py +372 -0
  143. switch_trust/scan/static_scanner.py +681 -0
  144. switch_trust/scan/taxonomy.py +121 -0
  145. switch_trust/scan/tool_dispatcher.py +773 -0
  146. switch_trust/scan/trace_logger.py +118 -0
  147. switch_trust/scan/trace_logger_file.py +143 -0
  148. switch_trust/scan/trace_logger_log.py +100 -0
  149. switch_trust/scan/triage.py +614 -0
  150. switch_trust/schema.py +79 -0
  151. switch_trust/secret_anonymizer.py +72 -0
  152. switch_trust_cli-1.7.0.dist-info/METADATA +495 -0
  153. switch_trust_cli-1.7.0.dist-info/RECORD +157 -0
  154. switch_trust_cli-1.7.0.dist-info/WHEEL +5 -0
  155. switch_trust_cli-1.7.0.dist-info/entry_points.txt +2 -0
  156. switch_trust_cli-1.7.0.dist-info/licenses/LICENSE +210 -0
  157. switch_trust_cli-1.7.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,315 @@
1
+ """
2
+ schema.py — Data models for the AI Agent Security Scanner
3
+ Mirrors the existing AI-SPM schema.py patterns.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import Any, Protocol, TypedDict
10
+
11
+ from switch_trust.schema import (
12
+ AffectedComponent, # noqa: F401
13
+ Evidence, # noqa: F401
14
+ )
15
+
16
+ # ── Protocols ────────────────────────────────────────────────────────────────
17
+
18
+
19
+ class InventoryLike(Protocol):
20
+ """Minimal interface for an Inventory object used by ScanConfig.
21
+
22
+ The full Inventory lives in the parent package
23
+ (switch_trust.schema). This protocol captures only what
24
+ the agent scanner reads, keeping the agent_scanner package decoupled.
25
+ """
26
+
27
+ @property
28
+ def agents(self) -> dict[str, Any]: ...
29
+
30
+
31
+ # ── TypedDicts ───────────────────────────────────────────────────────────────
32
+
33
+
34
+ class RawFinding(TypedDict):
35
+ """Shape of the finding dict produced by ToolDispatcher.report_finding()."""
36
+
37
+ category: str
38
+ subcategory: str
39
+ title: str
40
+ description: str
41
+ impact: str
42
+ remediation: str
43
+ affected_component: str
44
+ evidence: str
45
+ confidence: str
46
+ hallucination_flag: bool
47
+ evidence_file: str
48
+ evidence_line: int
49
+ agent_name: str
50
+
51
+
52
+ class PackageInfo(TypedDict):
53
+ """A pinned package extracted from requirements.txt."""
54
+
55
+ name: str
56
+ version: str
57
+
58
+
59
+ class CategorySummaryEntry(TypedDict):
60
+ """Per-category finding counts in ScanReport.category_summary."""
61
+
62
+ asi_code: str
63
+ asi_title: str
64
+ count: int
65
+ critical: int
66
+ high: int
67
+ medium: int
68
+ low: int
69
+
70
+
71
+ class TraceToolCall(TypedDict):
72
+ """Shape of a tool call entry recorded by TraceLogger."""
73
+
74
+ event: str
75
+ session_id: str
76
+ iteration: int
77
+ call_number: int
78
+ tool: str
79
+ args: dict[str, Any]
80
+ result_preview: str
81
+ result_length: int
82
+ tokens_consumed: int
83
+ tokens_total: int
84
+ duration_ms: int
85
+ timestamp: str
86
+ success: bool
87
+ error: str | None
88
+
89
+
90
+ # ── Config ───────────────────────────────────────────────────────────────────
91
+
92
+
93
+ @dataclass
94
+ class ScanConfig:
95
+ """Configuration for a single scan run.
96
+
97
+ Exactly one target must be set: local_path, files, or inventory.
98
+ When inventory is set, local_path should also be set (the folder the
99
+ inventory was parsed from) so agent files can be read from disk.
100
+ """
101
+
102
+ local_path: str | None = None
103
+ files: list[str] | None = None
104
+ inventory: InventoryLike | None = None
105
+
106
+ skip_triage: bool = False
107
+ agentic: bool = True
108
+
109
+ model_string: str | None = None
110
+
111
+
112
+ # ── Core data models ────────────────────────────────────────────────────────
113
+
114
+
115
+ @dataclass
116
+ class AgentProfile:
117
+ """
118
+ Extracted profile of a single AI agent from source code.
119
+ This is the core scan target — not the framework, the agent itself.
120
+ """
121
+
122
+ agent_id: str # Derived identifier (file + variable name)
123
+ framework: str # "crewai" | "autogen" | "langchain" | "custom_http"
124
+ source_file: str # Relative path in repo
125
+ role: str | None = None # Agent's stated role/purpose
126
+ goal: str | None = None # Agent's objective
127
+ backstory: str | None = None # Agent's backstory (CrewAI specific)
128
+ tools: list[str] = field(default_factory=list)
129
+ tool_details: list[str] = field(default_factory=list)
130
+ llm: str | None = None # LLM used (e.g. "gpt-4", "claude-3")
131
+ memory: bool | None = None # Memory enabled?
132
+ allow_delegation: bool | None = None # Can delegate to other agents?
133
+ verbose: bool | None = None
134
+ system_prompt: str | None = None # Inline system prompt if found
135
+ raw_code: str = "" # Relevant code block for AI reasoning
136
+ # ── v2 fields: guardrails and graph structure ─────────────────────────────
137
+ input_guardrails: list[str] = field(default_factory=list)
138
+ output_guardrails: list[str] = field(default_factory=list)
139
+ function_signatures: list[str] = field(default_factory=list)
140
+ graph_nodes: list[str] = field(default_factory=list)
141
+ has_recursion_limit: bool | None = None # StateGraph: recursion_limit set?
142
+
143
+
144
+ @dataclass
145
+ class TaskProfile:
146
+ """Extracted profile of a CrewAI Task or AutoGen task."""
147
+
148
+ task_id: str
149
+ source_file: str
150
+ description: str | None = None
151
+ expected_output: str | None = None
152
+ agent_ref: str | None = None # Which agent handles this task
153
+ tools: list[str] = field(default_factory=list)
154
+ human_input: bool | None = None
155
+ raw_code: str = ""
156
+
157
+
158
+ @dataclass
159
+ class CrewProfile:
160
+ """Extracted profile of a CrewAI Crew or AutoGen GroupChat."""
161
+
162
+ crew_id: str
163
+ source_file: str
164
+ agents: list[str] = field(default_factory=list)
165
+ tasks: list[str] = field(default_factory=list)
166
+ process: str | None = None # e.g. "sequential", "hierarchical"
167
+ memory: bool | None = None
168
+ raw_code: str = ""
169
+
170
+
171
+ @dataclass
172
+ class CvssScores:
173
+ """CVSS v4 scoring details."""
174
+
175
+ base_score: float = 0.0
176
+ vector: str = ""
177
+
178
+
179
+ @dataclass
180
+ class Finding:
181
+ """
182
+ A single security finding — unified format shared with MCP scanner.
183
+ Category and subcategory carry the taxonomy info (OWASP ASI or MCP taxonomy).
184
+ """
185
+
186
+ id: str # Unique finding identifier
187
+ category: str # Taxonomy category
188
+ subcategory: str # Specific vulnerability type
189
+ ai_spm_severity: str # Critical | High | Medium | Low
190
+ cvss_v4_severity: str # Critical | High | Medium | Low | None
191
+ cvss_scores: CvssScores # {base_score, vector}
192
+ description: str # Technical description of the issue
193
+ impact: str # What an attacker could achieve
194
+ likelihood: str # High | Medium | Low
195
+ remediation: str # Concrete fix guidance
196
+ affected_components: list[AffectedComponent] # Affected packages/files/modules
197
+ evidence: list[Evidence] # Structured evidence items
198
+ hallucination_flag: bool = False # True if AI finding lacks strong evidence
199
+ title: str = "" # Short human-readable title
200
+ source: str = "" # "static_bandit" | "static_opengrep" | "static_secrets"
201
+ # | "static_pip" | "ai_reasoning" | "mcp_scan"
202
+ agent_fingerprints: list[str] = field(
203
+ default_factory=list
204
+ ) # Agent fingerprints this finding belongs to
205
+ tool_fingerprints: list[str] = field(
206
+ default_factory=list
207
+ ) # Tools this finding's evidence falls within
208
+ agent_name: str = "" # Agent name/ID reported by the AI reasoning agent
209
+
210
+
211
+ # ── Trace models ─────────────────────────────────────────────────────────────
212
+
213
+
214
+ @dataclass
215
+ class ToolCall:
216
+ """
217
+ Records a single tool call made by the agentic reasoning loop.
218
+ Written to the scan trace for observability and debugging.
219
+ """
220
+
221
+ iteration: int
222
+ tool_name: str
223
+ tool_args: dict[str, Any]
224
+ result_preview: str # First 200 chars of the tool result
225
+ tokens_consumed: int # Approximate tokens in the result
226
+ duration_ms: int
227
+ timestamp: str
228
+
229
+
230
+ @dataclass
231
+ class ScanTrace:
232
+ """
233
+ Full observability record for an agentic reasoning session.
234
+ Written to <output_path>.trace.jsonl alongside the main report.
235
+ Allows replay of exactly what the agent investigated and in what order.
236
+ """
237
+
238
+ session_id: str
239
+ scan_timestamp: str
240
+ provider_model: str
241
+ total_iterations: int
242
+ total_tool_calls: int
243
+ total_tokens: int
244
+ wall_clock_ms: int
245
+ findings_count: int
246
+ tool_calls: list[ToolCall] = field(default_factory=list)
247
+ exit_reason: str = "completed" # completed | max_iterations | timeout | error
248
+
249
+
250
+ # ── Triage models ────────────────────────────────────────────────────────────
251
+
252
+
253
+ @dataclass
254
+ class TriageDismissed:
255
+ """
256
+ Records a finding that the triage agent dismissed as expected behaviour
257
+ for this tool/agent's declared purpose.
258
+ """
259
+
260
+ finding_id: str # Matches Finding.id
261
+ reason: str # Why the triage agent dismissed it
262
+
263
+
264
+ @dataclass
265
+ class TriageDowngraded:
266
+ """
267
+ Records a finding whose severity was reduced by the triage agent
268
+ due to lack of direct exploitability evidence or mitigating context.
269
+ """
270
+
271
+ finding_id: str # Matches Finding.id
272
+ original_severity: str # Severity assigned by the primary AI reasoning layer
273
+ new_severity: str # Recalibrated severity after triage
274
+ reason: str # Justification for the downgrade
275
+
276
+
277
+ # ── Report ───────────────────────────────────────────────────────────────────
278
+
279
+
280
+ @dataclass
281
+ class ScanReport:
282
+ """
283
+ Top-level output — one report per scanned repository.
284
+
285
+ Triage fields:
286
+ pre_triage_findings — raw findings list BEFORE triage runs (for auditability).
287
+ Populated only when triage runs; None otherwise.
288
+ triage_dismissed — findings the triage agent dismissed as expected behaviour.
289
+ triage_downgraded — findings whose severity was recalibrated downward.
290
+
291
+ The `findings` list always reflects the POST-triage state (filtered + recalibrated).
292
+ `category_summary` is also updated to reflect only post-triage findings.
293
+
294
+ Fix #18: schema_version is bumped whenever a breaking field change is made so that
295
+ downstream consumers (the AI-SPM platform database layer) can detect and migrate
296
+ old reports without guessing at the schema. Increment the minor version for additive
297
+ changes (new optional fields), the major version for breaking changes (removed or
298
+ renamed required fields).
299
+ """
300
+
301
+ # Serialises first so it always appears at the top of JSON output.
302
+ schema_version: str = "2.0"
303
+ repo_url: str = ""
304
+ repo_name: str = ""
305
+ scan_timestamp: str = ""
306
+ framework_detected: str = ""
307
+ agents_found: int = 0
308
+ findings: list[Finding] = field(default_factory=list)
309
+ category_summary: dict[str, CategorySummaryEntry] = field(default_factory=dict)
310
+ agent_profiles: list[dict[str, Any]] = field(default_factory=list)
311
+ scan_metadata: dict[str, Any] = field(default_factory=dict)
312
+ # ── Triage fields — None until triage.py runs ─────────────────────────────
313
+ pre_triage_findings: list[RawFinding] | None = None
314
+ triage_dismissed: list[TriageDismissed] | None = None
315
+ triage_downgraded: list[TriageDowngraded] | None = None
@@ -0,0 +1,372 @@
1
+ """
2
+ scorer.py — CVSS v4 Scoring Engine
3
+ Maps findings to CVSS v4 vectors and computes scores deterministically.
4
+ All category/subcategory keys align with the OWASP ASI taxonomy in taxonomy.py.
5
+ """
6
+
7
+ import logging
8
+
9
+ from cvss import CVSS4
10
+
11
+ from switch_trust.scan.taxonomy import get_finding_metadata
12
+
13
+ # CVSS v4 official severity bands (FIRST.org spec)
14
+ CVSS_SEVERITY_BANDS = [
15
+ (9.0, "critical"),
16
+ (7.0, "high"),
17
+ (4.0, "medium"),
18
+ (0.1, "low"),
19
+ (0.0, "info"),
20
+ ]
21
+
22
+
23
+ def severity_from_score(score: float) -> str:
24
+ """Derive severity label from CVSS v4 numeric score per FIRST.org bands.
25
+
26
+ Args:
27
+ score: CVSS v4 base score (0.0-10.0).
28
+
29
+ Returns:
30
+ Severity label: 'critical', 'high', 'medium', 'low', or 'info'.
31
+ Returns 'info' if score does not match any band.
32
+ """
33
+ for threshold, label in CVSS_SEVERITY_BANDS:
34
+ if score >= threshold:
35
+ return label
36
+ return "info"
37
+
38
+
39
+ def compute_cvss_v4_score(vector: str) -> float:
40
+ """
41
+ Compute CVSS v4 Base Score using the official FIRST.org library.
42
+
43
+ Args:
44
+ vector: CVSS v4 vector string (e.g., "CVSS:4.0/AV:N/AC:L/...")
45
+
46
+ Returns:
47
+ Base score (0.0–10.0) per official FIRST.org standard.
48
+ Returns 0.0 on invalid input; logs a warning in that case.
49
+ """
50
+ _log = logging.getLogger("scorer")
51
+
52
+ if not vector.startswith("CVSS:4.0/"):
53
+ _log.warning(
54
+ "[scorer] Invalid CVSS vector (wrong prefix): %r — defaulting to 0.0",
55
+ vector[:80],
56
+ )
57
+ return 0.0
58
+
59
+ try:
60
+ cvss_obj = CVSS4(vector)
61
+ return round(cvss_obj.base_score, 1)
62
+ except Exception as exc:
63
+ _log.warning(
64
+ "[scorer] CVSS4 computation failed for vector %r: %s — defaulting to 0.0",
65
+ vector[:80],
66
+ exc,
67
+ )
68
+ return 0.0
69
+
70
+
71
+ def score_finding(subcategory: str, source: str = "static") -> tuple[float, str, str]:
72
+ """
73
+ Look up CVSS vector for a subcategory, compute score, and return (score, vector, severity).
74
+
75
+ Severity label from taxonomy is authoritative — computed score is clamped into its band.
76
+
77
+ Args:
78
+ subcategory: ASI taxonomy subcategory key (e.g., 'hardcoded_credentials').
79
+ source: Finding source for logging context ('static', 'ai_reasoning', etc.).
80
+
81
+ Returns:
82
+ Tuple of (cvss_score, cvss_vector, severity_label).
83
+ cvss_score: float between 0.0 and 10.0 per CVSS v4 spec.
84
+ cvss_vector: CVSS:4.0 vector string.
85
+ severity_label: 'critical', 'high', 'medium', 'low', or 'info'.
86
+
87
+ Raises:
88
+ ValueError: If severity from taxonomy is not in recognized band_ranges.
89
+ """
90
+ meta = get_finding_metadata(subcategory)
91
+ vector = meta.get(
92
+ "cvss_vector", "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N"
93
+ )
94
+ severity = meta.get("severity", "medium").lower()
95
+
96
+ band_ranges = {
97
+ "critical": (9.0, 10.0),
98
+ "high": (7.0, 8.9),
99
+ "medium": (4.0, 6.9),
100
+ "low": (0.1, 3.9),
101
+ "info": (0.0, 0.0),
102
+ }
103
+
104
+ # Fix #7: Validate severity is in expected set before using it
105
+ if severity not in band_ranges:
106
+ logging.getLogger("scorer").warning(
107
+ f"[scorer] Invalid severity '{severity}' for {subcategory}. "
108
+ f"Valid: {list(band_ranges.keys())}. Defaulting to 'medium'."
109
+ )
110
+ severity = "medium"
111
+
112
+ band_min, band_max = band_ranges.get(severity, (4.0, 6.9))
113
+ raw_score = compute_cvss_v4_score(vector)
114
+ score = round(max(band_min, min(band_max, raw_score)), 1)
115
+ if score == 0.0 and severity != "info":
116
+ score = round((band_min + band_max) / 2, 1)
117
+
118
+ return score, vector, severity
119
+
120
+
121
+ # ── Bandit rule → (ASI category key, subcategory) ────────────────────────────
122
+
123
+
124
+ def map_bandit_to_taxonomy(bandit_rule_id: str, message: str) -> tuple[str, str]:
125
+ """Map a Bandit rule ID to the ASI-aligned taxonomy category/subcategory."""
126
+ rule_map = {
127
+ # ASI05 — Unexpected Code Execution
128
+ "B102": ("asi05_unexpected_code_execution", "arbitrary_code_execution"),
129
+ "B307": ("asi05_unexpected_code_execution", "arbitrary_code_execution"),
130
+ "B601": ("asi05_unexpected_code_execution", "arbitrary_code_execution"),
131
+ "B602": ("asi05_unexpected_code_execution", "arbitrary_code_execution"),
132
+ "B604": ("asi05_unexpected_code_execution", "arbitrary_code_execution"),
133
+ "B605": ("asi05_unexpected_code_execution", "arbitrary_code_execution"),
134
+ "B606": ("asi05_unexpected_code_execution", "arbitrary_code_execution"),
135
+ "B607": ("asi05_unexpected_code_execution", "arbitrary_code_execution"),
136
+ # ASI05 — unsafe deserialization
137
+ "B301": ("asi05_unexpected_code_execution", "unsafe_deserialization"),
138
+ "B302": ("asi05_unexpected_code_execution", "unsafe_deserialization"),
139
+ "B403": ("asi05_unexpected_code_execution", "unsafe_deserialization"),
140
+ # ASI02 — Tool misuse
141
+ "B603": ("asi02_tool_misuse", "unvalidated_tool_input"),
142
+ "B108": ("asi02_tool_misuse", "path_traversal"),
143
+ # ASI03 — Identity / credentials
144
+ "B105": ("asi03_identity_privilege_abuse", "hardcoded_credentials"),
145
+ "B106": ("asi03_identity_privilege_abuse", "hardcoded_credentials"),
146
+ "B107": ("asi03_identity_privilege_abuse", "hardcoded_credentials"),
147
+ "B501": ("asi03_identity_privilege_abuse", "missing_auth_on_endpoint"),
148
+ "B502": ("asi03_identity_privilege_abuse", "missing_auth_on_endpoint"),
149
+ "B503": ("asi03_identity_privilege_abuse", "missing_auth_on_endpoint"),
150
+ "B504": ("asi03_identity_privilege_abuse", "missing_auth_on_endpoint"),
151
+ # ASI01 — Goal hijack via template injection
152
+ "B701": ("asi01_agent_goal_hijack", "indirect_prompt_injection"),
153
+ "B702": ("asi01_agent_goal_hijack", "indirect_prompt_injection"),
154
+ # ASI06 — YAML unsafe load → memory/RAG poisoning vector
155
+ "B506": ("asi06_memory_context_poisoning", "memory_poisoning"),
156
+ }
157
+
158
+ # Keyword fallbacks
159
+ msg_lower = message.lower()
160
+ if (
161
+ "subprocess" in msg_lower
162
+ or "shell=true" in msg_lower
163
+ or "os.system" in msg_lower
164
+ ):
165
+ return ("asi05_unexpected_code_execution", "arbitrary_code_execution")
166
+ if "eval" in msg_lower or "exec" in msg_lower:
167
+ return ("asi05_unexpected_code_execution", "arbitrary_code_execution")
168
+ if "pickle" in msg_lower or "deserializ" in msg_lower or "yaml.load" in msg_lower:
169
+ return ("asi05_unexpected_code_execution", "unsafe_deserialization")
170
+ if "hardcoded" in msg_lower or "password" in msg_lower or "secret" in msg_lower:
171
+ return ("asi03_identity_privilege_abuse", "hardcoded_credentials")
172
+ if "tls" in msg_lower or "ssl" in msg_lower or "certificate" in msg_lower:
173
+ return ("asi03_identity_privilege_abuse", "missing_auth_on_endpoint")
174
+ if "sql" in msg_lower or "injection" in msg_lower:
175
+ return ("asi02_tool_misuse", "unvalidated_tool_input")
176
+
177
+ return rule_map.get(bandit_rule_id, ("asi02_tool_misuse", "unvalidated_tool_input"))
178
+
179
+
180
+ # ── OpenGrep rule → (ASI category key, subcategory) ──────────────────────────
181
+
182
+
183
+ def map_opengrep_to_taxonomy(rule_id: str) -> tuple[str, str]:
184
+ """Map an OpenGrep rule ID to the ASI-aligned taxonomy category/subcategory."""
185
+ rule_map = {
186
+ # ASI10
187
+ "crewai-allow-delegation-true": (
188
+ "asi10_rogue_agents",
189
+ "unchecked_agent_delegation",
190
+ ),
191
+ # ASI06
192
+ "crewai-memory-enabled": (
193
+ "asi06_memory_context_poisoning",
194
+ "persistent_memory_no_sanitization",
195
+ ),
196
+ "crewai-crew-memory-enabled": (
197
+ "asi06_memory_context_poisoning",
198
+ "cross_session_contamination",
199
+ ),
200
+ # ASI09
201
+ "autogen-human-input-never": (
202
+ "asi09_human_agent_trust_exploitation",
203
+ "no_human_in_the_loop",
204
+ ),
205
+ # ASI05
206
+ "autogen-code-execution-enabled": (
207
+ "asi05_unexpected_code_execution",
208
+ "arbitrary_code_execution",
209
+ ),
210
+ "subprocess-shell-true": (
211
+ "asi05_unexpected_code_execution",
212
+ "arbitrary_code_execution",
213
+ ),
214
+ "os-system-call": (
215
+ "asi05_unexpected_code_execution",
216
+ "arbitrary_code_execution",
217
+ ),
218
+ "eval-call": ("asi05_unexpected_code_execution", "arbitrary_code_execution"),
219
+ "unsafe-yaml-load": (
220
+ "asi05_unexpected_code_execution",
221
+ "unsafe_deserialization",
222
+ ),
223
+ "pickle-load": ("asi05_unexpected_code_execution", "unsafe_deserialization"),
224
+ # ASI01
225
+ "llm-input-fstring": ("asi01_agent_goal_hijack", "direct_prompt_injection"),
226
+ # ASI03
227
+ "hardcoded-api-key": (
228
+ "asi03_identity_privilege_abuse",
229
+ "hardcoded_credentials",
230
+ ),
231
+ "no-tls-server": ("asi03_identity_privilege_abuse", "missing_auth_on_endpoint"),
232
+ "server-binding-all-interfaces": (
233
+ "asi03_identity_privilege_abuse",
234
+ "missing_auth_on_endpoint",
235
+ ),
236
+ # ASI04
237
+ "unpinned-requirements": ("asi04_supply_chain", "unpinned_dependencies"),
238
+ "unpinned-ai-dependency": ("asi04_supply_chain", "unpinned_dependencies"),
239
+ # ASI07
240
+ "no-agent-message-auth": (
241
+ "asi07_insecure_interagent_comms",
242
+ "unauthenticated_agent_communication",
243
+ ),
244
+ "http-agent-endpoint": (
245
+ "asi07_insecure_interagent_comms",
246
+ "unencrypted_agent_channel",
247
+ ),
248
+ "unvalidated-agent-trust": (
249
+ "asi07_insecure_interagent_comms",
250
+ "unvalidated_agent_message",
251
+ ),
252
+ # ASI08
253
+ "no-max-iter": ("asi08_cascading_failures", "unbounded_agent_loop"),
254
+ "no-agent-timeout": ("asi08_cascading_failures", "unbounded_agent_loop"),
255
+ # ASI09
256
+ "no-confirmation-gate": (
257
+ "asi09_human_agent_trust_exploitation",
258
+ "missing_action_confirmation",
259
+ ),
260
+ # ASI10
261
+ "no-kill-switch": ("asi10_rogue_agents", "missing_kill_switch"),
262
+ "missing-agent-monitoring": ("asi10_rogue_agents", "missing_agent_monitoring"),
263
+ # Google ADK
264
+ "adk-loop-no-max-iterations": (
265
+ "asi08_cascading_failures",
266
+ "unbounded_agent_loop",
267
+ ),
268
+ "adk-agent-code-executor": (
269
+ "asi05_unexpected_code_execution",
270
+ "arbitrary_code_execution",
271
+ ),
272
+ "adk-agent-unchecked-delegation": (
273
+ "asi10_rogue_agents",
274
+ "unchecked_agent_delegation",
275
+ ),
276
+ "adk-agent-no-instruction": (
277
+ "asi01_agent_goal_hijack",
278
+ "goal_manipulation_via_rag",
279
+ ),
280
+ # OpenAI Agents SDK
281
+ "openai-runner-no-max-turns": (
282
+ "asi08_cascading_failures",
283
+ "unbounded_agent_loop",
284
+ ),
285
+ "openai-agent-no-input-guardrails": (
286
+ "asi01_agent_goal_hijack",
287
+ "direct_prompt_injection",
288
+ ),
289
+ "openai-agent-no-output-guardrails": (
290
+ "asi10_rogue_agents",
291
+ "missing_behavioral_guardrails",
292
+ ),
293
+ "openai-agent-unchecked-handoffs": (
294
+ "asi10_rogue_agents",
295
+ "unchecked_agent_delegation",
296
+ ),
297
+ # Anthropic
298
+ "anthropic-tool-loop-no-limit": (
299
+ "asi08_cascading_failures",
300
+ "unbounded_agent_loop",
301
+ ),
302
+ "anthropic-no-max-tokens": (
303
+ "asi08_cascading_failures",
304
+ "unbounded_agent_loop",
305
+ ),
306
+ "anthropic-tool-choice-any": (
307
+ "asi02_tool_misuse",
308
+ "excessive_tool_permissions",
309
+ ),
310
+ # LangGraph / LangChain
311
+ "langgraph-react-no-interrupt": (
312
+ "asi09_human_agent_trust_exploitation",
313
+ "missing_action_confirmation",
314
+ ),
315
+ "langchain-agent-no-max-iterations": (
316
+ "asi08_cascading_failures",
317
+ "unbounded_agent_loop",
318
+ ),
319
+ "langgraph-no-recursion-limit": (
320
+ "asi08_cascading_failures",
321
+ "unbounded_agent_loop",
322
+ ),
323
+ # Claude Agents SDK
324
+ "claude-query-no-max-turns": (
325
+ "asi08_cascading_failures",
326
+ "unbounded_agent_loop",
327
+ ),
328
+ "claude-client-no-max-turns": (
329
+ "asi08_cascading_failures",
330
+ "unbounded_agent_loop",
331
+ ),
332
+ "claude-options-no-system-prompt": (
333
+ "asi01_agent_goal_hijack",
334
+ "goal_manipulation_via_rag",
335
+ ),
336
+ "claude-options-bash-tool": (
337
+ "asi05_unexpected_code_execution",
338
+ "arbitrary_code_execution",
339
+ ),
340
+ "claude-subagent-no-prompt": (
341
+ "asi01_agent_goal_hijack",
342
+ "goal_manipulation_via_rag",
343
+ ),
344
+ "claude-subagent-bash-tool": (
345
+ "asi05_unexpected_code_execution",
346
+ "arbitrary_code_execution",
347
+ ),
348
+ # CrewAI (new rules)
349
+ "crewai-crew-no-max-iter": (
350
+ "asi08_cascading_failures",
351
+ "unbounded_agent_loop",
352
+ ),
353
+ "crewai-agent-verbose-false": (
354
+ "asi10_rogue_agents",
355
+ "missing_agent_monitoring",
356
+ ),
357
+ # AutoGen (new rules)
358
+ "autogen-groupchat-no-max-round": (
359
+ "asi08_cascading_failures",
360
+ "unbounded_agent_loop",
361
+ ),
362
+ "autogen-roundrobin-no-termination": (
363
+ "asi08_cascading_failures",
364
+ "unbounded_agent_loop",
365
+ ),
366
+ }
367
+
368
+ # Strip path prefix from rule ID (opengrep adds the rules file path)
369
+ short_id = rule_id.split(".")[-1] if "." in rule_id else rule_id
370
+ return rule_map.get(
371
+ short_id, rule_map.get(rule_id, ("asi02_tool_misuse", "unvalidated_tool_input"))
372
+ )