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.
- switch_trust/__init__.py +0 -0
- switch_trust/cli/__init__.py +0 -0
- switch_trust/cli/__main__.py +3 -0
- switch_trust/cli/builtin_config.json +879 -0
- switch_trust/cli/console.py +127 -0
- switch_trust/cli/eval_cli.py +905 -0
- switch_trust/cli/file_filter.py +172 -0
- switch_trust/cli/init_cli.py +156 -0
- switch_trust/cli/main.py +384 -0
- switch_trust/cli/output_formatters.py +482 -0
- switch_trust/cli/rich_observer.py +89 -0
- switch_trust/cli/runner.py +274 -0
- switch_trust/cli/scan_cli.py +314 -0
- switch_trust/cli/telemetry.py +430 -0
- switch_trust/cli/utils.py +158 -0
- switch_trust/cli/version.py +1 -0
- switch_trust/data/detector_prompts/llm01_adversarial.txt +59 -0
- switch_trust/data/detector_prompts/llm01_fixed.txt +6 -0
- switch_trust/data/detector_prompts/llm02_adversarial.txt +79 -0
- switch_trust/data/detector_prompts/llm02_fixed.txt +6 -0
- switch_trust/data/detector_prompts/llm05_adversarial.txt +67 -0
- switch_trust/data/detector_prompts/llm05_fixed.txt +14 -0
- switch_trust/data/detector_prompts/llm06_adversarial.txt +139 -0
- switch_trust/data/detector_prompts/llm06_fixed.txt +13 -0
- switch_trust/data/detector_prompts/llm07_adversarial.txt +189 -0
- switch_trust/data/detector_prompts/llm07_fixed.txt +12 -0
- switch_trust/data/detector_prompts/llm09_adversarial.txt +83 -0
- switch_trust/data/detector_prompts/llm09_fixed.txt +13 -0
- switch_trust/data/llm01_prompt_injection.csv +6708 -0
- switch_trust/data/llm02_sensitive_information.csv +1069 -0
- switch_trust/data/llm05_unsafe_output.csv +3882 -0
- switch_trust/data/llm06_excessive_agency.csv +778 -0
- switch_trust/data/llm07_system_prompt_leakage.csv +971 -0
- switch_trust/data/llm09_hallucination-goals.csv +32419 -0
- switch_trust/data/llm09_hallucination.csv +599 -0
- switch_trust/data/pii_leakage.csv +706 -0
- switch_trust/data/secret_leakage.csv +1380 -0
- switch_trust/eval/__init__.py +0 -0
- switch_trust/eval/common/__init__.py +0 -0
- switch_trust/eval/common/converter_anthropic.py +172 -0
- switch_trust/eval/common/converter_genai.py +171 -0
- switch_trust/eval/common/converter_openai.py +198 -0
- switch_trust/eval/common/log.py +116 -0
- switch_trust/eval/common/reference.py +25 -0
- switch_trust/eval/common/schema.py +190 -0
- switch_trust/eval/common/utils.py +119 -0
- switch_trust/eval/core/__init__.py +0 -0
- switch_trust/eval/core/detectors/__init__.py +0 -0
- switch_trust/eval/core/detectors/detector.py +25 -0
- switch_trust/eval/core/detectors/detector_garak.py +112 -0
- switch_trust/eval/core/detectors/detector_model.py +123 -0
- switch_trust/eval/core/detectors/detector_model_adversarial.py +91 -0
- switch_trust/eval/core/detectors/detector_pii.py +67 -0
- switch_trust/eval/core/detectors/detector_secret.py +88 -0
- switch_trust/eval/core/detectors/detector_topic_guard.py +63 -0
- switch_trust/eval/core/detectors/detector_toxicity.py +78 -0
- switch_trust/eval/core/eval/__init__.py +0 -0
- switch_trust/eval/core/eval/eval_creator.py +166 -0
- switch_trust/eval/core/eval/evaluation.py +112 -0
- switch_trust/eval/core/eval/evaluation_adversarial.py +412 -0
- switch_trust/eval/core/eval/evaluation_garak_module.py +45 -0
- switch_trust/eval/core/eval/evaluation_garak_probe.py +308 -0
- switch_trust/eval/core/eval/evaluation_message_list.py +46 -0
- switch_trust/eval/core/eval/evaluation_multi.py +276 -0
- switch_trust/eval/core/eval/evaluation_single.py +89 -0
- switch_trust/eval/core/eval/evaluation_single_prompt.py +62 -0
- switch_trust/eval/core/eval/evaluation_topic_guard.py +305 -0
- switch_trust/eval/core/eval/metric_conciseness.py +179 -0
- switch_trust/eval/core/eval/metric_factual_accuracy.py +312 -0
- switch_trust/eval/core/eval/metric_instruction_adherence.py +177 -0
- switch_trust/eval/core/eval/metric_tone.py +180 -0
- switch_trust/eval/core/eval/metric_toxicity.py +146 -0
- switch_trust/eval/core/eval/observer.py +137 -0
- switch_trust/eval/core/message/__init__.py +0 -0
- switch_trust/eval/core/message/message_collection.py +37 -0
- switch_trust/eval/core/message/message_collection_csv.py +44 -0
- switch_trust/eval/core/message/message_collection_garak.py +46 -0
- switch_trust/eval/core/message/message_collection_memory.py +34 -0
- switch_trust/eval/core/models/__init__.py +0 -0
- switch_trust/eval/core/models/generator_model.py +421 -0
- switch_trust/eval/core/models/model.py +172 -0
- switch_trust/eval/core/models/model_adk.py +186 -0
- switch_trust/eval/core/models/model_anthropic.py +79 -0
- switch_trust/eval/core/models/model_anthropic_agent.py +104 -0
- switch_trust/eval/core/models/model_gemini.py +146 -0
- switch_trust/eval/core/models/model_generic_http.py +120 -0
- switch_trust/eval/core/models/model_http.py +135 -0
- switch_trust/eval/core/models/model_huggingface.py +75 -0
- switch_trust/eval/core/models/model_langserve.py +96 -0
- switch_trust/eval/core/models/model_litellm.py +52 -0
- switch_trust/eval/core/models/model_ollama.py +65 -0
- switch_trust/eval/core/models/model_openai.py +71 -0
- switch_trust/eval/core/models/model_openai_agent.py +92 -0
- switch_trust/eval/core/models/model_openai_compatible.py +66 -0
- switch_trust/eval/core/models/model_per_endpoint_limiter.py +80 -0
- switch_trust/eval/core/models/model_retry.py +204 -0
- switch_trust/eval/core/models/model_sync_wrapper.py +38 -0
- switch_trust/eval/core/models/model_vertex_agent_runtime.py +315 -0
- switch_trust/eval/core/models/response_schema.py +148 -0
- switch_trust/eval/core/optional_deps.py +57 -0
- switch_trust/eval/db/__init__.py +0 -0
- switch_trust/eval/db/base/__init__.py +0 -0
- switch_trust/eval/db/base/common_types.py +16 -0
- switch_trust/eval/db/base/detectors/__init__.py +0 -0
- switch_trust/eval/db/base/detectors/detector_helpers.py +88 -0
- switch_trust/eval/db/base/detectors/detector_repository.py +96 -0
- switch_trust/eval/db/base/detectors/detector_types.py +77 -0
- switch_trust/eval/db/base/eval/__init__.py +0 -0
- switch_trust/eval/db/base/eval/eval_helpers.py +186 -0
- switch_trust/eval/db/base/eval/eval_repository.py +120 -0
- switch_trust/eval/db/base/eval/eval_run.py +142 -0
- switch_trust/eval/db/base/eval/eval_types.py +127 -0
- switch_trust/eval/db/base/eval/model_eval_repository.py +79 -0
- switch_trust/eval/db/base/eval/model_eval_run_repository.py +72 -0
- switch_trust/eval/db/base/eval/model_eval_run_result_repository.py +42 -0
- switch_trust/eval/db/base/eval/model_eval_run_result_types.py +21 -0
- switch_trust/eval/db/base/eval/model_eval_run_types.py +78 -0
- switch_trust/eval/db/base/eval/model_eval_types.py +69 -0
- switch_trust/eval/db/base/message/__init__.py +0 -0
- switch_trust/eval/db/base/message/message_collection_helpers.py +50 -0
- switch_trust/eval/db/base/message/message_collection_repository.py +95 -0
- switch_trust/eval/db/base/message/message_collection_types.py +87 -0
- switch_trust/eval/db/base/models/__init__.py +0 -0
- switch_trust/eval/db/base/models/model_helpers.py +253 -0
- switch_trust/eval/db/base/models/model_repository.py +104 -0
- switch_trust/eval/db/base/models/model_types.py +159 -0
- switch_trust/eval/db/json/__init__.py +0 -0
- switch_trust/eval/db/json/repository_json.py +559 -0
- switch_trust/scan/__init__.py +12 -0
- switch_trust/scan/agent_scanner.py +812 -0
- switch_trust/scan/config/agent_opengrep_rules.yaml +533 -0
- switch_trust/scan/config/agent_taxonomy.json +517 -0
- switch_trust/scan/config/agentic_cvss_mapping.yaml +136 -0
- switch_trust/scan/config/compliance_mappings.json +110 -0
- switch_trust/scan/config/triage_prompt.txt +326 -0
- switch_trust/scan/constants.py +15 -0
- switch_trust/scan/exception_classification.py +179 -0
- switch_trust/scan/llm_provider.py +330 -0
- switch_trust/scan/opengrep_resolver.py +12 -0
- switch_trust/scan/reasoner.py +602 -0
- switch_trust/scan/schema.py +315 -0
- switch_trust/scan/scorer.py +372 -0
- switch_trust/scan/static_scanner.py +681 -0
- switch_trust/scan/taxonomy.py +121 -0
- switch_trust/scan/tool_dispatcher.py +773 -0
- switch_trust/scan/trace_logger.py +118 -0
- switch_trust/scan/trace_logger_file.py +143 -0
- switch_trust/scan/trace_logger_log.py +100 -0
- switch_trust/scan/triage.py +614 -0
- switch_trust/schema.py +79 -0
- switch_trust/secret_anonymizer.py +72 -0
- switch_trust_cli-1.7.0.dist-info/METADATA +495 -0
- switch_trust_cli-1.7.0.dist-info/RECORD +157 -0
- switch_trust_cli-1.7.0.dist-info/WHEEL +5 -0
- switch_trust_cli-1.7.0.dist-info/entry_points.txt +2 -0
- switch_trust_cli-1.7.0.dist-info/licenses/LICENSE +210 -0
- 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
|
+
)
|