actshield 0.9.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.
- actshield/__init__.py +439 -0
- actshield/__main__.py +46 -0
- actshield/agents/__init__.py +12 -0
- actshield/agents/agent.py +200 -0
- actshield/agents/identity.py +114 -0
- actshield/api/__init__.py +34 -0
- actshield/api/models.py +198 -0
- actshield/api/server.py +669 -0
- actshield/api/service.py +599 -0
- actshield/approval/__init__.py +15 -0
- actshield/approval/approval.py +164 -0
- actshield/cli/__init__.py +8 -0
- actshield/cli/graph.py +49 -0
- actshield/cli/main.py +802 -0
- actshield/cli/shell.py +406 -0
- actshield/cli/theme.py +110 -0
- actshield/client.py +757 -0
- actshield/config.py +88 -0
- actshield/context/__init__.py +14 -0
- actshield/context/context.py +161 -0
- actshield/context/provenance.py +108 -0
- actshield/context/taint.py +48 -0
- actshield/decisions/decision.py +74 -0
- actshield/delegation/__init__.py +11 -0
- actshield/delegation/authority.py +63 -0
- actshield/delegation/delegation.py +180 -0
- actshield/demo.py +139 -0
- actshield/doctor.py +94 -0
- actshield/drift/__init__.py +27 -0
- actshield/drift/drift_engine.py +149 -0
- actshield/drift/drift_models.py +63 -0
- actshield/evaluation/__init__.py +1 -0
- actshield/evaluation/cases/__init__.py +1 -0
- actshield/evaluation/eval_suite.py +254 -0
- actshield/forensics/__init__.py +53 -0
- actshield/forensics/access_graph.py +216 -0
- actshield/forensics/authority_graph.py +186 -0
- actshield/forensics/delegation_graph.py +119 -0
- actshield/forensics/explanation.py +254 -0
- actshield/forensics/models.py +488 -0
- actshield/forensics/queries.py +367 -0
- actshield/forensics/serializers.py +68 -0
- actshield/forensics/service.py +310 -0
- actshield/forensics/snapshots.py +174 -0
- actshield/gates/__init__.py +13 -0
- actshield/gates/gate_models.py +46 -0
- actshield/gates/security_gate.py +191 -0
- actshield/gateway/__init__.py +13 -0
- actshield/gateway/http.py +191 -0
- actshield/gateway/mcp.py +292 -0
- actshield/incidents/__init__.py +25 -0
- actshield/incidents/incident.py +94 -0
- actshield/incidents/incident_engine.py +163 -0
- actshield/incidents/incident_rules.py +116 -0
- actshield/incidents/incident_state.py +58 -0
- actshield/integrations/__init__.py +24 -0
- actshield/integrations/ai_secura.py +169 -0
- actshield/integrations/ai_secura_analysis.py +236 -0
- actshield/integrations/apiris.py +122 -0
- actshield/integrations/few_shot.py +104 -0
- actshield/integrations/ollama_adapter.py +538 -0
- actshield/integrations/prompts/__init__.py +1 -0
- actshield/integrations/prompts/examples/__init__.py +1 -0
- actshield/integrations/security_packet.py +82 -0
- actshield/llm_config.py +89 -0
- actshield/offensive/__init__.py +95 -0
- actshield/offensive/adaptive/__init__.py +22 -0
- actshield/offensive/adaptive/adaptive_engine.py +165 -0
- actshield/offensive/adaptive/attack_selector.py +51 -0
- actshield/offensive/adaptive/boundary_search.py +99 -0
- actshield/offensive/adaptive/campaign_memory.py +123 -0
- actshield/offensive/adaptive/feedback.py +84 -0
- actshield/offensive/adaptive/mutation_planner.py +57 -0
- actshield/offensive/attack.py +78 -0
- actshield/offensive/campaign.py +219 -0
- actshield/offensive/corpus.py +155 -0
- actshield/offensive/engine.py +466 -0
- actshield/offensive/evaluator.py +116 -0
- actshield/offensive/evidence.py +63 -0
- actshield/offensive/generator.py +128 -0
- actshield/offensive/mutation.py +101 -0
- actshield/offensive/results.py +77 -0
- actshield/offensive/safety.py +80 -0
- actshield/offensive/strategies/__init__.py +22 -0
- actshield/offensive/strategies/authority_escalation.py +100 -0
- actshield/offensive/strategies/base.py +34 -0
- actshield/offensive/strategies/context_manipulation.py +100 -0
- actshield/offensive/strategies/data_exfiltration.py +100 -0
- actshield/offensive/strategies/delegation_attacks.py +100 -0
- actshield/offensive/strategies/mcp_attacks.py +100 -0
- actshield/offensive/strategies/prompt_injection.py +100 -0
- actshield/offensive/strategies/tool_poisoning.py +100 -0
- actshield/offensive/target.py +113 -0
- actshield/offensive/vulnerable_target.py +121 -0
- actshield/persistence/__init__.py +18 -0
- actshield/persistence/siem.py +102 -0
- actshield/persistence/storage.py +240 -0
- actshield/policy/__init__.py +12 -0
- actshield/policy/evaluator.py +294 -0
- actshield/policy/intent.py +113 -0
- actshield/policy/policy.py +48 -0
- actshield/posture/__init__.py +26 -0
- actshield/posture/posture_engine.py +186 -0
- actshield/posture/posture_models.py +152 -0
- actshield/posture/posture_rules.py +137 -0
- actshield/providers/__init__.py +32 -0
- actshield/providers/anthropic_provider.py +146 -0
- actshield/providers/base.py +160 -0
- actshield/providers/gemini_provider.py +133 -0
- actshield/providers/null_provider.py +40 -0
- actshield/providers/ollama_provider.py +135 -0
- actshield/providers/openai_provider.py +142 -0
- actshield/providers/registry.py +244 -0
- actshield/py.typed +0 -0
- actshield/response/__init__.py +14 -0
- actshield/response/response_actions.py +39 -0
- actshield/response/response_audit.py +32 -0
- actshield/response/response_engine.py +139 -0
- actshield/risk/models.py +104 -0
- actshield/serve.py +117 -0
- actshield/static/404.html +1 -0
- actshield/static/__next.__PAGE__.txt +24 -0
- actshield/static/__next._full.txt +20 -0
- actshield/static/__next._tree.txt +4 -0
- actshield/static/_next/static/Yp_swxN-C9UoCRNOwkYnY/_buildManifest.js +11 -0
- actshield/static/_next/static/Yp_swxN-C9UoCRNOwkYnY/_clientMiddlewareManifest.js +1 -0
- actshield/static/_next/static/Yp_swxN-C9UoCRNOwkYnY/_ssgManifest.js +1 -0
- actshield/static/_next/static/chunks/09kny2eqjc2or.js +1 -0
- actshield/static/_next/static/chunks/0c1zba1u9whdp.js +1 -0
- actshield/static/_next/static/chunks/0cz1d0mv5g_q7.js +1 -0
- actshield/static/_next/static/chunks/0dbjjeg01x25o.js +1 -0
- actshield/static/_next/static/chunks/0duobs55ne238.js +1 -0
- actshield/static/_next/static/chunks/0iaz41l6068e4.js +1 -0
- actshield/static/_next/static/chunks/0mjhyv72x2idp.js +1 -0
- actshield/static/_next/static/chunks/0nelpqfu_ciq6.js +1 -0
- actshield/static/_next/static/chunks/0p1bci9ch797v.js +1 -0
- actshield/static/_next/static/chunks/0tld_ygho5tgm.js +1 -0
- actshield/static/_next/static/chunks/0w1-y7oni66j9.js +1 -0
- actshield/static/_next/static/chunks/10537zyl427zj.js +1 -0
- actshield/static/_next/static/chunks/11pc_jkl3aggn.js +1 -0
- actshield/static/_next/static/chunks/16k7nw0b3t8ys.js +1 -0
- actshield/static/_next/static/chunks/19mx3mg6lkumu.js +1 -0
- actshield/static/_next/static/chunks/1_8p47b-kxg2a.js +1 -0
- actshield/static/_next/static/chunks/1dszjozwfqegx.js +1 -0
- actshield/static/_next/static/chunks/1h43jcb90-m2r.js +22 -0
- actshield/static/_next/static/chunks/1ly04cvt_wh_j.js +1 -0
- actshield/static/_next/static/chunks/1odxrypuwuwp_.js +1 -0
- actshield/static/_next/static/chunks/1oes4c35qz5ku.js +1 -0
- actshield/static/_next/static/chunks/1ow883jl4ln-r.js +16 -0
- actshield/static/_next/static/chunks/1po7bna1as8tc.js +1 -0
- actshield/static/_next/static/chunks/1rsh0og8ifkzx.js +1 -0
- actshield/static/_next/static/chunks/1v0lwzv-859z_.js +1 -0
- actshield/static/_next/static/chunks/25iat2epwx25a.js +1 -0
- actshield/static/_next/static/chunks/25uvt4s2f39a4.js +1 -0
- actshield/static/_next/static/chunks/27t_qfc-3_lzs.js +1 -0
- actshield/static/_next/static/chunks/28bkmy8mez7gm.js +1 -0
- actshield/static/_next/static/chunks/2b6brmw_drw1p.js +1 -0
- actshield/static/_next/static/chunks/2fnhtxi4j6vge.js +1 -0
- actshield/static/_next/static/chunks/2p087eftxemg4.js +6 -0
- actshield/static/_next/static/chunks/2s8pao_z8pgtq.js +1 -0
- actshield/static/_next/static/chunks/2yj7ooc2c-zpg.js +1 -0
- actshield/static/_next/static/chunks/3-mrrr8udt5tk.js +1 -0
- actshield/static/_next/static/chunks/310vm2bl3xxpt.js +31 -0
- actshield/static/_next/static/chunks/35mvsv251jx1r.css +3 -0
- actshield/static/_next/static/chunks/370vf0oetlsx9.js +1 -0
- actshield/static/_next/static/chunks/3e-2m72tyo556.js +1 -0
- actshield/static/_next/static/chunks/3fntmmi971322.js +1 -0
- actshield/static/_next/static/chunks/3fzw-g0hycl2c.js +1 -0
- actshield/static/_next/static/chunks/3lopq3lhmcjxg.js +129 -0
- actshield/static/_next/static/chunks/3mpe2jejel0v2.css +1 -0
- actshield/static/_next/static/chunks/3t8o4f2lzr2sf.js +1 -0
- actshield/static/_next/static/chunks/3y-v1neyllne5.js +1 -0
- actshield/static/_next/static/chunks/43oiwy8mx6cs1.js +1 -0
- actshield/static/_next/static/chunks/turbopack-23wzuebcy_lxm.js +1 -0
- actshield/static/_next/static/media/1bffadaabf893a1e-s.3-6t-g6q0vh0a.woff2 +0 -0
- actshield/static/_next/static/media/2bbe8d2671613f1f-s.0k62hbripvv8p.woff2 +0 -0
- actshield/static/_next/static/media/2c55a0e60120577a-s.0-dom-5bn10r2.woff2 +0 -0
- actshield/static/_next/static/media/5476f68d60460930-s.2uwcyprjm3xu3.woff2 +0 -0
- actshield/static/_next/static/media/83afe278b6a6bb3c-s.p.2bn3s6zvc0dyp.woff2 +0 -0
- actshield/static/_next/static/media/9c72aa0f40e4eef8-s.1y4-pdgsjb-pw.woff2 +0 -0
- actshield/static/_next/static/media/ad66f9afd8947f86-s.3lvt2whj97whp.woff2 +0 -0
- actshield/static/_next/static/media/favicon.2vob68tjqpejf.ico +0 -0
- actshield/static/_not-found/__next._full.txt +17 -0
- actshield/static/_not-found/__next._not-found/__PAGE__.txt +23 -0
- actshield/static/_not-found/__next._tree.txt +3 -0
- actshield/static/_not-found.html +1 -0
- actshield/static/_not-found.txt +17 -0
- actshield/static/access/matrix/__next._full.txt +24 -0
- actshield/static/access/matrix/__next._tree.txt +4 -0
- actshield/static/access/matrix/__next.access/matrix/__PAGE__.txt +30 -0
- actshield/static/access/matrix.html +1 -0
- actshield/static/access/matrix.txt +24 -0
- actshield/static/activity/__next._full.txt +22 -0
- actshield/static/activity/__next._tree.txt +4 -0
- actshield/static/activity/__next.activity/__PAGE__.txt +27 -0
- actshield/static/activity.html +1 -0
- actshield/static/activity.txt +22 -0
- actshield/static/agents/__next._full.txt +22 -0
- actshield/static/agents/__next._tree.txt +4 -0
- actshield/static/agents/__next.agents/__PAGE__.txt +27 -0
- actshield/static/agents.html +1 -0
- actshield/static/agents.txt +22 -0
- actshield/static/ai-secura/__next._full.txt +22 -0
- actshield/static/ai-secura/__next._tree.txt +4 -0
- actshield/static/ai-secura/__next.ai-secura/__PAGE__.txt +27 -0
- actshield/static/ai-secura.html +1 -0
- actshield/static/ai-secura.txt +22 -0
- actshield/static/api/__next._full.txt +22 -0
- actshield/static/api/__next._tree.txt +4 -0
- actshield/static/api/__next.api/__PAGE__.txt +27 -0
- actshield/static/api.html +15 -0
- actshield/static/api.txt +22 -0
- actshield/static/apiris/__next._full.txt +22 -0
- actshield/static/apiris/__next._tree.txt +4 -0
- actshield/static/apiris/__next.apiris/__PAGE__.txt +27 -0
- actshield/static/apiris.html +1 -0
- actshield/static/apiris.txt +22 -0
- actshield/static/attack-graph/__next._full.txt +23 -0
- actshield/static/attack-graph/__next._tree.txt +5 -0
- actshield/static/attack-graph/__next.attack-graph/__PAGE__.txt +28 -0
- actshield/static/attack-graph.html +2 -0
- actshield/static/attack-graph.txt +23 -0
- actshield/static/campaigns/__next._full.txt +22 -0
- actshield/static/campaigns/__next._tree.txt +4 -0
- actshield/static/campaigns/__next.campaigns/__PAGE__.txt +27 -0
- actshield/static/campaigns.html +1 -0
- actshield/static/campaigns.txt +22 -0
- actshield/static/context/__next._full.txt +22 -0
- actshield/static/context/__next._tree.txt +4 -0
- actshield/static/context/__next.context/__PAGE__.txt +27 -0
- actshield/static/context.html +1 -0
- actshield/static/context.txt +22 -0
- actshield/static/dashboard/__next._full.txt +22 -0
- actshield/static/dashboard/__next._tree.txt +4 -0
- actshield/static/dashboard/__next.dashboard/__PAGE__.txt +27 -0
- actshield/static/dashboard.html +1 -0
- actshield/static/dashboard.txt +22 -0
- actshield/static/delegations/__next._full.txt +22 -0
- actshield/static/delegations/__next._tree.txt +4 -0
- actshield/static/delegations/__next.delegations/__PAGE__.txt +27 -0
- actshield/static/delegations.html +1 -0
- actshield/static/delegations.txt +22 -0
- actshield/static/demo/__next._full.txt +22 -0
- actshield/static/demo/__next._tree.txt +4 -0
- actshield/static/demo/__next.demo/__PAGE__.txt +27 -0
- actshield/static/demo.html +1 -0
- actshield/static/demo.txt +22 -0
- actshield/static/drift/__next._full.txt +22 -0
- actshield/static/drift/__next._tree.txt +4 -0
- actshield/static/drift/__next.drift/__PAGE__.txt +27 -0
- actshield/static/drift.html +1 -0
- actshield/static/drift.txt +22 -0
- actshield/static/favicon.ico +0 -0
- actshield/static/file.svg +1 -0
- actshield/static/forensics/__next._full.txt +22 -0
- actshield/static/forensics/__next._tree.txt +4 -0
- actshield/static/forensics/__next.forensics/__PAGE__.txt +27 -0
- actshield/static/forensics.html +1 -0
- actshield/static/forensics.txt +22 -0
- actshield/static/globe.svg +1 -0
- actshield/static/incidents/__next._full.txt +22 -0
- actshield/static/incidents/__next._tree.txt +4 -0
- actshield/static/incidents/__next.incidents/__PAGE__.txt +27 -0
- actshield/static/incidents.html +1 -0
- actshield/static/incidents.txt +22 -0
- actshield/static/index.html +3 -0
- actshield/static/index.txt +20 -0
- actshield/static/logo.png +0 -0
- actshield/static/metrics/__next._full.txt +22 -0
- actshield/static/metrics/__next._tree.txt +4 -0
- actshield/static/metrics/__next.metrics/__PAGE__.txt +27 -0
- actshield/static/metrics.html +1 -0
- actshield/static/metrics.txt +22 -0
- actshield/static/next.svg +1 -0
- actshield/static/offensive/__next._full.txt +22 -0
- actshield/static/offensive/__next._tree.txt +4 -0
- actshield/static/offensive/__next.offensive/__PAGE__.txt +27 -0
- actshield/static/offensive.html +1 -0
- actshield/static/offensive.txt +22 -0
- actshield/static/policies/__next._full.txt +22 -0
- actshield/static/policies/__next._tree.txt +4 -0
- actshield/static/policies/__next.policies/__PAGE__.txt +27 -0
- actshield/static/policies.html +1 -0
- actshield/static/policies.txt +22 -0
- actshield/static/posture/__next._full.txt +22 -0
- actshield/static/posture/__next._tree.txt +4 -0
- actshield/static/posture/__next.posture/__PAGE__.txt +27 -0
- actshield/static/posture.html +1 -0
- actshield/static/posture.txt +22 -0
- actshield/static/regressions/__next._full.txt +22 -0
- actshield/static/regressions/__next._tree.txt +4 -0
- actshield/static/regressions/__next.regressions/__PAGE__.txt +27 -0
- actshield/static/regressions.html +1 -0
- actshield/static/regressions.txt +22 -0
- actshield/static/reports/__next._full.txt +22 -0
- actshield/static/reports/__next._tree.txt +4 -0
- actshield/static/reports/__next.reports/__PAGE__.txt +27 -0
- actshield/static/reports.html +1 -0
- actshield/static/reports.txt +22 -0
- actshield/static/resources/__next._full.txt +22 -0
- actshield/static/resources/__next._tree.txt +4 -0
- actshield/static/resources/__next.resources/__PAGE__.txt +27 -0
- actshield/static/resources.html +1 -0
- actshield/static/resources.txt +22 -0
- actshield/static/security-gates/__next._full.txt +22 -0
- actshield/static/security-gates/__next._tree.txt +4 -0
- actshield/static/security-gates/__next.security-gates/__PAGE__.txt +27 -0
- actshield/static/security-gates.html +1 -0
- actshield/static/security-gates.txt +22 -0
- actshield/static/settings/__next._full.txt +22 -0
- actshield/static/settings/__next._tree.txt +4 -0
- actshield/static/settings/__next.settings/__PAGE__.txt +27 -0
- actshield/static/settings.html +1 -0
- actshield/static/settings.txt +22 -0
- actshield/static/tasks/__next._full.txt +22 -0
- actshield/static/tasks/__next._tree.txt +4 -0
- actshield/static/tasks/__next.tasks/__PAGE__.txt +27 -0
- actshield/static/tasks.html +1 -0
- actshield/static/tasks.txt +22 -0
- actshield/static/threats/__next._full.txt +22 -0
- actshield/static/threats/__next._tree.txt +4 -0
- actshield/static/threats/__next.threats/__PAGE__.txt +27 -0
- actshield/static/threats.html +1 -0
- actshield/static/threats.txt +22 -0
- actshield/static/tools/__next._full.txt +22 -0
- actshield/static/tools/__next._tree.txt +4 -0
- actshield/static/tools/__next.tools/__PAGE__.txt +27 -0
- actshield/static/tools.html +1 -0
- actshield/static/tools.txt +22 -0
- actshield/static/traces/__next._full.txt +22 -0
- actshield/static/traces/__next._tree.txt +4 -0
- actshield/static/traces/__next.traces/__PAGE__.txt +27 -0
- actshield/static/traces.html +1 -0
- actshield/static/traces.txt +22 -0
- actshield/static/vercel.svg +1 -0
- actshield/static/window.svg +1 -0
- actshield/tasks/__init__.py +6 -0
- actshield/tasks/task.py +143 -0
- actshield/threatmodel/__init__.py +70 -0
- actshield/threatmodel/actors.py +210 -0
- actshield/threatmodel/analyzer.py +131 -0
- actshield/threatmodel/assets.py +167 -0
- actshield/threatmodel/boundaries.py +165 -0
- actshield/threatmodel/exporters.py +49 -0
- actshield/threatmodel/mitigations.py +209 -0
- actshield/threatmodel/models.py +80 -0
- actshield/threatmodel/report.py +341 -0
- actshield/threatmodel/scenarios.py +212 -0
- actshield/threatmodel/scoring.py +66 -0
- actshield/threatmodel/threats.py +387 -0
- actshield/tools/__init__.py +20 -0
- actshield/tools/interceptor.py +256 -0
- actshield/tools/tool.py +71 -0
- actshield/tracing/__init__.py +38 -0
- actshield/tracing/correlation.py +173 -0
- actshield/tracing/events.py +75 -0
- actshield/tracing/tracer.py +305 -0
- actshield/watch.py +129 -0
- actshield-0.9.0.dist-info/METADATA +57 -0
- actshield-0.9.0.dist-info/RECORD +366 -0
- actshield-0.9.0.dist-info/WHEEL +5 -0
- actshield-0.9.0.dist-info/entry_points.txt +4 -0
- actshield-0.9.0.dist-info/top_level.txt +2 -0
- agentguard/__init__.py +39 -0
- agentguard/__main__.py +8 -0
- agentguard/py.typed +1 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Threat model exporters — JSON, YAML, Markdown."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from actshield.threatmodel.models import AnalysisResult
|
|
8
|
+
from actshield.threatmodel.report import ThreatReportRenderer
|
|
9
|
+
|
|
10
|
+
ExportFormat = Literal["json", "yaml", "markdown"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ThreatModelExporter:
|
|
14
|
+
"""Exports threat analysis results to external formats."""
|
|
15
|
+
|
|
16
|
+
def export(self, result: AnalysisResult, format: ExportFormat = "json") -> str:
|
|
17
|
+
if format == "json":
|
|
18
|
+
return self._to_json(result)
|
|
19
|
+
elif format == "yaml":
|
|
20
|
+
return self._to_yaml(result)
|
|
21
|
+
elif format == "markdown":
|
|
22
|
+
renderer = ThreatReportRenderer()
|
|
23
|
+
return renderer.to_markdown(result)
|
|
24
|
+
else:
|
|
25
|
+
raise ValueError(f"Unsupported export format: {format}. Use json, yaml, or markdown.")
|
|
26
|
+
|
|
27
|
+
def _to_json(self, result: AnalysisResult) -> str:
|
|
28
|
+
return json.dumps(result.to_dict(), indent=2, default=str)
|
|
29
|
+
|
|
30
|
+
def _to_yaml(self, result: AnalysisResult) -> str:
|
|
31
|
+
try:
|
|
32
|
+
import yaml # type: ignore[import]
|
|
33
|
+
return yaml.dump(result.to_dict(), default_flow_style=False, allow_unicode=True)
|
|
34
|
+
except ImportError:
|
|
35
|
+
# Fallback: minimal YAML-like output without PyYAML
|
|
36
|
+
data = result.to_dict()
|
|
37
|
+
lines = ["# ActShield Threat Model Export (YAML)"]
|
|
38
|
+
lines.append(f"overall_risk_score: {data.get('overall_risk_score', 0)}")
|
|
39
|
+
lines.append("model:")
|
|
40
|
+
model = data.get("model", {})
|
|
41
|
+
for k, v in model.items():
|
|
42
|
+
lines.append(f" {k}: {json.dumps(v)}")
|
|
43
|
+
lines.append("threats_by_severity:")
|
|
44
|
+
for sev, threats in data.get("threats_by_severity", {}).items():
|
|
45
|
+
lines.append(f" {sev}: {len(threats)} threat(s)")
|
|
46
|
+
lines.append(
|
|
47
|
+
"# Install PyYAML for full YAML export: pip install pyyaml"
|
|
48
|
+
)
|
|
49
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Control-to-threat mapping — how ActShield controls mitigate each threat."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
from actshield.threatmodel.models import ControlEffectiveness
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Mitigation(BaseModel):
|
|
10
|
+
"""A single ActShield control and its relationship to a threat."""
|
|
11
|
+
control_id: str
|
|
12
|
+
control_name: str
|
|
13
|
+
description: str
|
|
14
|
+
# How the control works mechanically
|
|
15
|
+
mechanism: str
|
|
16
|
+
# Enforcement action
|
|
17
|
+
enforcement_action: str # BLOCK | MONITOR | DETECT | ALERT
|
|
18
|
+
# Where in the ActShield pipeline this runs
|
|
19
|
+
pipeline_stage: str # provenance | taint | intent | authority | policy | evidence
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ControlMapping(BaseModel):
|
|
23
|
+
"""Maps a threat to the set of ActShield controls that mitigate it."""
|
|
24
|
+
threat_id: str
|
|
25
|
+
controls: list[Mitigation] = Field(default_factory=list)
|
|
26
|
+
combined_effectiveness: ControlEffectiveness = ControlEffectiveness.REDUCES
|
|
27
|
+
notes: str = ""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ── ActShield control definitions ─────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
ACTSHIELD_CONTROLS: dict[str, Mitigation] = {
|
|
33
|
+
"provenance_tracking": Mitigation(
|
|
34
|
+
control_id="ctl_provenance",
|
|
35
|
+
control_name="Context Provenance Tracking",
|
|
36
|
+
description=(
|
|
37
|
+
"Every piece of context entering the agent system is tagged with its "
|
|
38
|
+
"origin source, trust level, and a provenance chain."
|
|
39
|
+
),
|
|
40
|
+
mechanism=(
|
|
41
|
+
"ProvenanceHop records are attached to each context element. "
|
|
42
|
+
"External sources receive EXTERNAL trust level."
|
|
43
|
+
),
|
|
44
|
+
enforcement_action="DETECT",
|
|
45
|
+
pipeline_stage="provenance",
|
|
46
|
+
),
|
|
47
|
+
"taint_tracking": Mitigation(
|
|
48
|
+
control_id="ctl_taint",
|
|
49
|
+
control_name="Taint Propagation",
|
|
50
|
+
description=(
|
|
51
|
+
"Context derived from untrusted or external sources is marked TAINTED "
|
|
52
|
+
"and that taint propagates through the reasoning chain."
|
|
53
|
+
),
|
|
54
|
+
mechanism=(
|
|
55
|
+
"TaintState.TAINTED is set on context from external sources. "
|
|
56
|
+
"Tainted context cannot authorize CRITICAL tool execution."
|
|
57
|
+
),
|
|
58
|
+
enforcement_action="BLOCK",
|
|
59
|
+
pipeline_stage="taint",
|
|
60
|
+
),
|
|
61
|
+
"mcp_gateway": Mitigation(
|
|
62
|
+
control_id="ctl_mcp",
|
|
63
|
+
control_name="MCP Gateway Interception",
|
|
64
|
+
description="All MCP server communications are intercepted and inspected.",
|
|
65
|
+
mechanism=(
|
|
66
|
+
"MCPGateway wraps all MCP calls. Responses are analyzed, "
|
|
67
|
+
"taint-marked, and provenance-recorded before entering agent context."
|
|
68
|
+
),
|
|
69
|
+
enforcement_action="BLOCK",
|
|
70
|
+
pipeline_stage="provenance",
|
|
71
|
+
),
|
|
72
|
+
"http_gateway": Mitigation(
|
|
73
|
+
control_id="ctl_http",
|
|
74
|
+
control_name="HTTP Gateway Interception",
|
|
75
|
+
description="All external HTTP calls are intercepted and responses are taint-marked.",
|
|
76
|
+
mechanism=(
|
|
77
|
+
"HTTPGateway intercepts outbound requests and inbound responses. "
|
|
78
|
+
"External responses marked EXTERNAL and TAINTED."
|
|
79
|
+
),
|
|
80
|
+
enforcement_action="DETECT",
|
|
81
|
+
pipeline_stage="provenance",
|
|
82
|
+
),
|
|
83
|
+
"intent_analysis": Mitigation(
|
|
84
|
+
control_id="ctl_intent",
|
|
85
|
+
control_name="Intent Analysis (AI Secura / Deterministic)",
|
|
86
|
+
description=(
|
|
87
|
+
"AI-powered and deterministic intent analysis classifies whether "
|
|
88
|
+
"a tool request is aligned with the declared task."
|
|
89
|
+
),
|
|
90
|
+
mechanism=(
|
|
91
|
+
"DeterministicIntentAnalyzer checks keywords, patterns, and taint. "
|
|
92
|
+
"AI Secura provides probabilistic scoring. Results inform policy."
|
|
93
|
+
),
|
|
94
|
+
enforcement_action="ALERT",
|
|
95
|
+
pipeline_stage="intent",
|
|
96
|
+
),
|
|
97
|
+
"authority_containment": Mitigation(
|
|
98
|
+
control_id="ctl_authority",
|
|
99
|
+
control_name="Authority Containment",
|
|
100
|
+
description=(
|
|
101
|
+
"Agents cannot claim authority beyond what was explicitly delegated. "
|
|
102
|
+
"The containment invariant: effective ≤ delegated ≤ declared."
|
|
103
|
+
),
|
|
104
|
+
mechanism=(
|
|
105
|
+
"Effective authority is computed from the delegation chain. "
|
|
106
|
+
"Tool requests requiring authority exceeding effective are blocked."
|
|
107
|
+
),
|
|
108
|
+
enforcement_action="BLOCK",
|
|
109
|
+
pipeline_stage="authority",
|
|
110
|
+
),
|
|
111
|
+
"effective_authority_check": Mitigation(
|
|
112
|
+
control_id="ctl_eff_auth",
|
|
113
|
+
control_name="Effective Authority Verification",
|
|
114
|
+
description="Verifies the computed effective authority before any tool execution.",
|
|
115
|
+
mechanism=(
|
|
116
|
+
"DECLARED → DELEGATED → EFFECTIVE chain is computed and compared "
|
|
117
|
+
"against required tool authority."
|
|
118
|
+
),
|
|
119
|
+
enforcement_action="BLOCK",
|
|
120
|
+
pipeline_stage="authority",
|
|
121
|
+
),
|
|
122
|
+
"delegation_tracking": Mitigation(
|
|
123
|
+
control_id="ctl_delegation",
|
|
124
|
+
control_name="Delegation Chain Tracking",
|
|
125
|
+
description="Full delegation graph is maintained and verified for every agent action.",
|
|
126
|
+
mechanism="Delegation records are immutable and cryptographically linked.",
|
|
127
|
+
enforcement_action="DETECT",
|
|
128
|
+
pipeline_stage="authority",
|
|
129
|
+
),
|
|
130
|
+
"policy_evaluation": Mitigation(
|
|
131
|
+
control_id="ctl_policy",
|
|
132
|
+
control_name="Deterministic Policy Evaluation",
|
|
133
|
+
description=(
|
|
134
|
+
"The PolicyEvaluator applies rules deterministically — AI advice "
|
|
135
|
+
"is input to policy, not the policy itself."
|
|
136
|
+
),
|
|
137
|
+
mechanism=(
|
|
138
|
+
"PolicyEvaluator evaluates RuleCondition sets. "
|
|
139
|
+
"Output: ALLOW | MONITOR | HITL | QUARANTINE | BLOCK | REVOKE."
|
|
140
|
+
),
|
|
141
|
+
enforcement_action="BLOCK",
|
|
142
|
+
pipeline_stage="policy",
|
|
143
|
+
),
|
|
144
|
+
"tool_interception": Mitigation(
|
|
145
|
+
control_id="ctl_tool",
|
|
146
|
+
control_name="Tool Interception (@protect decorator)",
|
|
147
|
+
description="Protected tools cannot execute without passing the full ActShield pipeline.",
|
|
148
|
+
mechanism=(
|
|
149
|
+
"@guard.protect() wraps tool functions. All executions pass through "
|
|
150
|
+
"provenance → taint → intent → authority → policy before execution."
|
|
151
|
+
),
|
|
152
|
+
enforcement_action="BLOCK",
|
|
153
|
+
pipeline_stage="policy",
|
|
154
|
+
),
|
|
155
|
+
"audit_logging": Mitigation(
|
|
156
|
+
control_id="ctl_audit",
|
|
157
|
+
control_name="Structured Audit Logging",
|
|
158
|
+
description="Every security decision produces a structured, immutable audit record.",
|
|
159
|
+
mechanism=(
|
|
160
|
+
"SecurityEvent records are written with: who, what, when, why, "
|
|
161
|
+
"context, authority, tool, resource, decision, outcome."
|
|
162
|
+
),
|
|
163
|
+
enforcement_action="DETECT",
|
|
164
|
+
pipeline_stage="evidence",
|
|
165
|
+
),
|
|
166
|
+
"source_attribution": Mitigation(
|
|
167
|
+
control_id="ctl_attribution",
|
|
168
|
+
control_name="Source Attribution",
|
|
169
|
+
description="Context elements are attributed to their originating source at retrieval time.",
|
|
170
|
+
mechanism="ProvenanceHop records source URL, trust level, and retrieval timestamp.",
|
|
171
|
+
enforcement_action="DETECT",
|
|
172
|
+
pipeline_stage="provenance",
|
|
173
|
+
),
|
|
174
|
+
"sensitivity_classification": Mitigation(
|
|
175
|
+
control_id="ctl_sensitivity",
|
|
176
|
+
control_name="Tool Sensitivity Classification",
|
|
177
|
+
description="Tools are classified by sensitivity level (LOW → CRITICAL).",
|
|
178
|
+
mechanism=(
|
|
179
|
+
"SensitivityLevel is declared on ToolDefinition. "
|
|
180
|
+
"CRITICAL tools require authority + clean taint + explicit policy."
|
|
181
|
+
),
|
|
182
|
+
enforcement_action="BLOCK",
|
|
183
|
+
pipeline_stage="policy",
|
|
184
|
+
),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def get_control(control_id: str) -> Mitigation | None:
|
|
189
|
+
return ACTSHIELD_CONTROLS.get(control_id)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def get_all_controls() -> list[Mitigation]:
|
|
193
|
+
return list(ACTSHIELD_CONTROLS.values())
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def build_control_coverage_map(threat_control_ids: list[str]) -> dict[str, bool]:
|
|
197
|
+
"""Return which ActShield control domains are covered."""
|
|
198
|
+
domains = {
|
|
199
|
+
"Identity": any(c in threat_control_ids for c in ["provenance_tracking", "delegation_tracking"]),
|
|
200
|
+
"Authority": any(c in threat_control_ids for c in ["authority_containment", "effective_authority_check", "delegation_tracking"]),
|
|
201
|
+
"Context": any(c in threat_control_ids for c in ["taint_tracking", "provenance_tracking", "source_attribution"]),
|
|
202
|
+
"Tool Security": any(c in threat_control_ids for c in ["tool_interception", "sensitivity_classification"]),
|
|
203
|
+
"MCP": any(c in threat_control_ids for c in ["mcp_gateway"]),
|
|
204
|
+
"Data Access": any(c in threat_control_ids for c in ["authority_containment", "policy_evaluation"]),
|
|
205
|
+
"Delegation": any(c in threat_control_ids for c in ["delegation_tracking", "authority_containment"]),
|
|
206
|
+
"Enforcement": any(c in threat_control_ids for c in ["policy_evaluation"]),
|
|
207
|
+
"Evidence": any(c in threat_control_ids for c in ["audit_logging"]),
|
|
208
|
+
}
|
|
209
|
+
return domains
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Threat modeling core models — ThreatModel, AnalysisResult, enumerations."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ThreatSeverity(str, Enum):
|
|
13
|
+
"""CVSS-inspired severity classification."""
|
|
14
|
+
CRITICAL = "CRITICAL"
|
|
15
|
+
HIGH = "HIGH"
|
|
16
|
+
MEDIUM = "MEDIUM"
|
|
17
|
+
LOW = "LOW"
|
|
18
|
+
INFORMATIONAL = "INFORMATIONAL"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ThreatCategory(str, Enum):
|
|
22
|
+
"""Top-level threat category aligned to STRIDE + MITRE ATLAS concepts."""
|
|
23
|
+
# STRIDE
|
|
24
|
+
SPOOFING = "SPOOFING" # Identity / agent impersonation
|
|
25
|
+
TAMPERING = "TAMPERING" # Context or data tampering
|
|
26
|
+
REPUDIATION = "REPUDIATION" # Lack of audit trail
|
|
27
|
+
INFORMATION_DISCLOSURE = "INFO_DISCLOSURE" # Data exfiltration
|
|
28
|
+
DENIAL_OF_SERVICE = "DENIAL_OF_SERVICE"
|
|
29
|
+
ELEVATION_OF_PRIVILEGE = "PRIVILEGE_ESCALATION"
|
|
30
|
+
# MITRE ATLAS / Agentic AI specific
|
|
31
|
+
PROMPT_INJECTION = "PROMPT_INJECTION"
|
|
32
|
+
INDIRECT_PROMPT_INJECTION = "INDIRECT_PROMPT_INJECTION"
|
|
33
|
+
TOOL_POISONING = "TOOL_POISONING"
|
|
34
|
+
CONTEXT_POISONING = "CONTEXT_POISONING"
|
|
35
|
+
DELEGATION_ABUSE = "DELEGATION_ABUSE"
|
|
36
|
+
CREDENTIAL_THEFT = "CREDENTIAL_THEFT"
|
|
37
|
+
MCP_COMPROMISE = "MCP_COMPROMISE"
|
|
38
|
+
SUPPLY_CHAIN = "SUPPLY_CHAIN"
|
|
39
|
+
RAG_POISONING = "RAG_POISONING"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ControlEffectiveness(str, Enum):
|
|
43
|
+
"""How effectively the mapped ActShield control mitigates the threat."""
|
|
44
|
+
PREVENTS = "PREVENTS" # Control fully prevents exploitation
|
|
45
|
+
DETECTS = "DETECTS" # Control detects but may not prevent
|
|
46
|
+
REDUCES = "REDUCES" # Control reduces likelihood / impact
|
|
47
|
+
MONITORS = "MONITORS" # Control provides observability only
|
|
48
|
+
NONE = "NONE" # No current control coverage
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ThreatModel(BaseModel):
|
|
52
|
+
"""Container for the full threat model of a deployed agent system."""
|
|
53
|
+
model_id: str = Field(default_factory=lambda: f"tm_{uuid.uuid4().hex[:8]}")
|
|
54
|
+
system_name: str = "ActShield-Monitored System"
|
|
55
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
56
|
+
version: str = "1.0.0"
|
|
57
|
+
|
|
58
|
+
# Populated by the registry/analyzer
|
|
59
|
+
asset_count: int = 0
|
|
60
|
+
actor_count: int = 0
|
|
61
|
+
boundary_count: int = 0
|
|
62
|
+
threat_count: int = 0
|
|
63
|
+
scenario_count: int = 0
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> dict[str, Any]:
|
|
66
|
+
return self.model_dump(mode="json")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class AnalysisResult(BaseModel):
|
|
70
|
+
"""Complete output of a ThreatAnalyzer.analyze() run."""
|
|
71
|
+
model: ThreatModel
|
|
72
|
+
threats_by_severity: dict[str, list[dict[str, Any]]] = Field(default_factory=dict)
|
|
73
|
+
control_coverage: dict[str, bool] = Field(default_factory=dict)
|
|
74
|
+
residual_risks: list[dict[str, Any]] = Field(default_factory=list)
|
|
75
|
+
attack_paths: list[dict[str, Any]] = Field(default_factory=list)
|
|
76
|
+
analysis_timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
77
|
+
overall_risk_score: float = 0.0 # 0-10 scale
|
|
78
|
+
|
|
79
|
+
def to_dict(self) -> dict[str, Any]:
|
|
80
|
+
return self.model_dump(mode="json")
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Threat model Rich CLI renderer — professional security report output."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
from rich.tree import Tree
|
|
8
|
+
from rich.columns import Columns
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
from rich import box
|
|
11
|
+
|
|
12
|
+
from actshield.cli.theme import ACTSHIELD_THEME
|
|
13
|
+
from actshield.threatmodel.models import (
|
|
14
|
+
ThreatModel,
|
|
15
|
+
AnalysisResult,
|
|
16
|
+
ThreatSeverity,
|
|
17
|
+
ControlEffectiveness,
|
|
18
|
+
)
|
|
19
|
+
from actshield.threatmodel.threats import Threat
|
|
20
|
+
|
|
21
|
+
console = Console(theme=ACTSHIELD_THEME)
|
|
22
|
+
|
|
23
|
+
# Severity → Rich style
|
|
24
|
+
_SEV_STYLE: dict[str, str] = {
|
|
25
|
+
ThreatSeverity.CRITICAL.value: "ag.critical",
|
|
26
|
+
ThreatSeverity.HIGH.value: "ag.attack",
|
|
27
|
+
ThreatSeverity.MEDIUM.value: "ag.warning",
|
|
28
|
+
ThreatSeverity.LOW.value: "ag.monitor",
|
|
29
|
+
ThreatSeverity.INFORMATIONAL.value: "ag.dim",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_EFF_LABEL: dict[ControlEffectiveness, str] = {
|
|
33
|
+
ControlEffectiveness.PREVENTS: "✓ PREVENTS",
|
|
34
|
+
ControlEffectiveness.DETECTS: "◉ DETECTS",
|
|
35
|
+
ControlEffectiveness.REDUCES: "◈ REDUCES",
|
|
36
|
+
ControlEffectiveness.MONITORS: "○ MONITORS",
|
|
37
|
+
ControlEffectiveness.NONE: "✗ NONE",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_EFF_STYLE: dict[ControlEffectiveness, str] = {
|
|
41
|
+
ControlEffectiveness.PREVENTS: "ag.healthy",
|
|
42
|
+
ControlEffectiveness.DETECTS: "ag.monitor",
|
|
43
|
+
ControlEffectiveness.REDUCES: "ag.warning",
|
|
44
|
+
ControlEffectiveness.MONITORS: "ag.hitl",
|
|
45
|
+
ControlEffectiveness.NONE: "ag.violation",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ThreatReportRenderer:
|
|
50
|
+
"""Renders threat model data as Rich terminal output."""
|
|
51
|
+
|
|
52
|
+
# ── Summary ──────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
def render_model_summary(self, model: ThreatModel) -> None:
|
|
55
|
+
console.print()
|
|
56
|
+
console.print(Panel(
|
|
57
|
+
f"[ag.header]System[/ag.header] {model.system_name}\n"
|
|
58
|
+
f"[ag.header]Model ID[/ag.header] [ag.id]{model.model_id}[/ag.id]\n"
|
|
59
|
+
f"[ag.header]Version[/ag.header] {model.version}\n\n"
|
|
60
|
+
f"[ag.label]Assets [/ag.label][bold]{model.asset_count}[/bold]\n"
|
|
61
|
+
f"[ag.label]Threat Actors [/ag.label][bold]{model.actor_count}[/bold]\n"
|
|
62
|
+
f"[ag.label]Trust Boundaries[/ag.label][bold]{model.boundary_count}[/bold]\n"
|
|
63
|
+
f"[ag.label]Threats [/ag.label][bold]{model.threat_count}[/bold]\n"
|
|
64
|
+
f"[ag.label]Attack Scenarios[/ag.label][bold]{model.scenario_count}[/bold]",
|
|
65
|
+
title="[ag.brand]ACTSHIELD THREAT MODEL[/ag.brand]",
|
|
66
|
+
border_style="rgb(59,130,246)",
|
|
67
|
+
expand=False,
|
|
68
|
+
))
|
|
69
|
+
console.print()
|
|
70
|
+
|
|
71
|
+
# ── Threat list ──────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
def render_threat_list(self, threats: list[Threat]) -> None:
|
|
74
|
+
table = Table(
|
|
75
|
+
box=box.SIMPLE_HEAD,
|
|
76
|
+
show_header=True,
|
|
77
|
+
header_style="ag.header",
|
|
78
|
+
pad_edge=False,
|
|
79
|
+
expand=False,
|
|
80
|
+
)
|
|
81
|
+
table.add_column("ID", style="ag.id", no_wrap=True)
|
|
82
|
+
table.add_column("Name", min_width=32)
|
|
83
|
+
table.add_column("Category", style="ag.label")
|
|
84
|
+
table.add_column("Severity", no_wrap=True)
|
|
85
|
+
table.add_column("Control Effectiveness", no_wrap=True)
|
|
86
|
+
table.add_column("Entry Point(s)", style="ag.dim")
|
|
87
|
+
|
|
88
|
+
for t in sorted(
|
|
89
|
+
threats,
|
|
90
|
+
key=lambda x: list(ThreatSeverity).index(x.severity)
|
|
91
|
+
):
|
|
92
|
+
sev_style = _SEV_STYLE.get(t.severity.value, "")
|
|
93
|
+
eff_label = _EFF_LABEL.get(t.control_effectiveness, "?")
|
|
94
|
+
eff_style = _EFF_STYLE.get(t.control_effectiveness, "")
|
|
95
|
+
table.add_row(
|
|
96
|
+
t.threat_id,
|
|
97
|
+
t.name,
|
|
98
|
+
t.category.value.replace("_", " "),
|
|
99
|
+
Text(t.severity.value, style=sev_style),
|
|
100
|
+
Text(eff_label, style=eff_style),
|
|
101
|
+
", ".join(t.entry_points[:2]),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
console.print()
|
|
105
|
+
console.print(table)
|
|
106
|
+
console.print(f" [ag.dim]{len(threats)} threat(s)[/ag.dim]")
|
|
107
|
+
console.print()
|
|
108
|
+
|
|
109
|
+
# ── Threat detail ────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
def render_threat_detail(self, threat: Threat) -> None:
|
|
112
|
+
sev_style = _SEV_STYLE.get(threat.severity.value, "")
|
|
113
|
+
eff_label = _EFF_LABEL.get(threat.control_effectiveness, "?")
|
|
114
|
+
eff_style = _EFF_STYLE.get(threat.control_effectiveness, "")
|
|
115
|
+
|
|
116
|
+
# Attack path tree
|
|
117
|
+
path_tree = Tree("[ag.label]Attack Path[/ag.label]")
|
|
118
|
+
for step in threat.attack_path:
|
|
119
|
+
path_tree.add(f"[ag.dim]{step}[/ag.dim]")
|
|
120
|
+
|
|
121
|
+
# Controls table
|
|
122
|
+
controls_text = "\n".join(
|
|
123
|
+
f" [ag.healthy]·[/ag.healthy] {c.replace('_', ' ').title()}"
|
|
124
|
+
for c in threat.actshield_controls
|
|
125
|
+
) or " [ag.violation]No controls mapped[/ag.violation]"
|
|
126
|
+
|
|
127
|
+
content = (
|
|
128
|
+
f"[ag.label]ID [/ag.label] {threat.threat_id}\n"
|
|
129
|
+
f"[ag.label]Category [/ag.label] {threat.category.value.replace('_', ' ')}\n"
|
|
130
|
+
f"[ag.label]Severity [/ag.label] [{sev_style}]{threat.severity.value}[/{sev_style}]\n"
|
|
131
|
+
f"[ag.label]STRIDE [/ag.label] {threat.stride_category or 'N/A'}\n"
|
|
132
|
+
f"[ag.label]MITRE Atlas[/ag.label] {threat.mitre_atlas_id or 'N/A'}\n\n"
|
|
133
|
+
f"[ag.header]Description[/ag.header]\n {threat.description}\n\n"
|
|
134
|
+
f"[ag.header]ActShield Controls[/ag.header]\n{controls_text}\n\n"
|
|
135
|
+
f"[ag.header]Control Effectiveness[/ag.header]\n [{eff_style}]{eff_label}[/{eff_style}]\n\n"
|
|
136
|
+
f"[ag.header]Residual Risk[/ag.header]\n {threat.residual_risk}\n\n"
|
|
137
|
+
f"[ag.header]Evidence Generated[/ag.header]\n "
|
|
138
|
+
+ ", ".join(threat.evidence_generated)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
console.print()
|
|
142
|
+
console.print(Panel(
|
|
143
|
+
content,
|
|
144
|
+
title=f"[ag.brand]{threat.name}[/ag.brand]",
|
|
145
|
+
border_style="rgb(59,130,246)",
|
|
146
|
+
expand=False,
|
|
147
|
+
))
|
|
148
|
+
console.print()
|
|
149
|
+
console.print(path_tree)
|
|
150
|
+
console.print()
|
|
151
|
+
|
|
152
|
+
# ── Full analysis report ─────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
def render_analysis_report(self, result: AnalysisResult) -> None:
|
|
155
|
+
model = result.model
|
|
156
|
+
sev_counts = {s.value: 0 for s in ThreatSeverity}
|
|
157
|
+
for sev, threats in result.threats_by_severity.items():
|
|
158
|
+
sev_counts[sev] = len(threats)
|
|
159
|
+
|
|
160
|
+
console.print()
|
|
161
|
+
# Header panel
|
|
162
|
+
console.print(Panel(
|
|
163
|
+
f"[ag.label]System [/ag.label]{model.system_name}\n"
|
|
164
|
+
f"[ag.label]Analysis Time [/ag.label]{result.analysis_timestamp.strftime('%Y-%m-%d %H:%M:%S UTC')}\n"
|
|
165
|
+
f"[ag.label]Overall Risk [/ag.label][bold]{result.overall_risk_score:.1f}[/bold] / 10.0\n",
|
|
166
|
+
title="[ag.brand]ACTSHIELD THREAT ANALYSIS[/ag.brand]",
|
|
167
|
+
border_style="rgb(59,130,246)",
|
|
168
|
+
expand=False,
|
|
169
|
+
))
|
|
170
|
+
|
|
171
|
+
# Counts row
|
|
172
|
+
console.print()
|
|
173
|
+
console.print(" [ag.header]Threat Inventory[/ag.header]")
|
|
174
|
+
counts_table = Table(box=box.SIMPLE, show_header=False, pad_edge=False)
|
|
175
|
+
counts_table.add_column("", style="ag.label", no_wrap=True)
|
|
176
|
+
counts_table.add_column("", style="bold", no_wrap=True)
|
|
177
|
+
counts_table.add_row("Assets", str(model.asset_count))
|
|
178
|
+
counts_table.add_row("Trust Boundaries", str(model.boundary_count))
|
|
179
|
+
counts_table.add_row("Threat Actors", str(model.actor_count))
|
|
180
|
+
counts_table.add_row("Identified Threats", str(model.threat_count))
|
|
181
|
+
counts_table.add_row("Attack Scenarios", str(model.scenario_count))
|
|
182
|
+
console.print(counts_table)
|
|
183
|
+
|
|
184
|
+
# Severity breakdown
|
|
185
|
+
console.print()
|
|
186
|
+
console.print(" [ag.header]Severity Breakdown[/ag.header]")
|
|
187
|
+
sev_table = Table(box=box.SIMPLE, show_header=False, pad_edge=False)
|
|
188
|
+
sev_table.add_column("", no_wrap=True)
|
|
189
|
+
sev_table.add_column("", no_wrap=True)
|
|
190
|
+
for sev, style in _SEV_STYLE.items():
|
|
191
|
+
count = sev_counts.get(sev, 0)
|
|
192
|
+
bar = "█" * count
|
|
193
|
+
sev_table.add_row(
|
|
194
|
+
Text(f" {sev:<14}", style=style),
|
|
195
|
+
Text(f"{count:>3} {bar}", style=style),
|
|
196
|
+
)
|
|
197
|
+
console.print(sev_table)
|
|
198
|
+
|
|
199
|
+
# Coverage map
|
|
200
|
+
console.print()
|
|
201
|
+
console.print(" [ag.header]Control Domain Coverage[/ag.header]")
|
|
202
|
+
cov_table = Table(box=box.SIMPLE, show_header=False, pad_edge=False)
|
|
203
|
+
cov_table.add_column("", style="ag.label", no_wrap=True)
|
|
204
|
+
cov_table.add_column("", no_wrap=True)
|
|
205
|
+
for domain, covered in result.control_coverage.items():
|
|
206
|
+
mark = Text(" ✓", style="ag.healthy") if covered else Text(" ✗", style="ag.violation")
|
|
207
|
+
cov_table.add_row(f" {domain:<24}", mark)
|
|
208
|
+
console.print(cov_table)
|
|
209
|
+
|
|
210
|
+
# Attack paths
|
|
211
|
+
if result.attack_paths:
|
|
212
|
+
console.print()
|
|
213
|
+
console.print(" [ag.header]Attack Scenarios[/ag.header]")
|
|
214
|
+
for path in result.attack_paths:
|
|
215
|
+
sev_style = _SEV_STYLE.get(path["severity"], "")
|
|
216
|
+
enforcement = path["enforcement"]
|
|
217
|
+
enf_style = "ag.block" if enforcement == "BLOCK" else "ag.monitor"
|
|
218
|
+
console.print(
|
|
219
|
+
f" [{sev_style}]{path['severity']:<10}[/{sev_style}]"
|
|
220
|
+
f" {path['name']:<52}"
|
|
221
|
+
f" [{enf_style}]{enforcement}[/{enf_style}]"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Residual risks
|
|
225
|
+
if result.residual_risks:
|
|
226
|
+
console.print()
|
|
227
|
+
console.print(" [ag.header]Residual Risks[/ag.header]")
|
|
228
|
+
for risk in result.residual_risks:
|
|
229
|
+
sev_style = _SEV_STYLE.get(risk["severity"], "")
|
|
230
|
+
console.print(
|
|
231
|
+
f" [{sev_style}]{risk['severity']:<10}[/{sev_style}]"
|
|
232
|
+
f" {risk['name']:<40}"
|
|
233
|
+
f" score={risk['residual_score']:.1f}"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
console.print()
|
|
237
|
+
|
|
238
|
+
# ── Attack graph (Rich tree) ──────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
def render_attack_graph(self, result: AnalysisResult) -> None:
|
|
241
|
+
from actshield.threatmodel.scenarios import get_builtin_scenarios
|
|
242
|
+
scenarios = get_builtin_scenarios()
|
|
243
|
+
|
|
244
|
+
console.print()
|
|
245
|
+
root = Tree("[ag.brand]ACTSHIELD ATTACK GRAPH[/ag.brand]")
|
|
246
|
+
for scenario in scenarios:
|
|
247
|
+
sev_style = _SEV_STYLE.get(scenario.severity.value, "")
|
|
248
|
+
scenario_node = root.add(
|
|
249
|
+
f"[{sev_style}]{scenario.severity.value}[/{sev_style}] {scenario.name}"
|
|
250
|
+
)
|
|
251
|
+
for step in scenario.steps:
|
|
252
|
+
outcome_style = (
|
|
253
|
+
"ag.healthy" if step.outcome == "blocked"
|
|
254
|
+
else "ag.warning" if step.outcome == "detected"
|
|
255
|
+
else "ag.dim"
|
|
256
|
+
)
|
|
257
|
+
label = f"[{outcome_style}]{step.outcome.upper():>8}[/{outcome_style}] Step {step.step_num}: {step.description[:60]}"
|
|
258
|
+
scenario_node.add(label)
|
|
259
|
+
console.print(root)
|
|
260
|
+
console.print()
|
|
261
|
+
|
|
262
|
+
# ── Markdown export ───────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
def to_markdown(self, result: AnalysisResult) -> str:
|
|
265
|
+
model = result.model
|
|
266
|
+
lines = [
|
|
267
|
+
f"# ActShield Threat Model Report",
|
|
268
|
+
f"",
|
|
269
|
+
f"**System:** {model.system_name} ",
|
|
270
|
+
f"**Date:** {result.analysis_timestamp.strftime('%Y-%m-%d %H:%M UTC')} ",
|
|
271
|
+
f"**Overall Risk Score:** {result.overall_risk_score:.1f} / 10.0 ",
|
|
272
|
+
f"**Model ID:** `{model.model_id}`",
|
|
273
|
+
f"",
|
|
274
|
+
f"---",
|
|
275
|
+
f"",
|
|
276
|
+
f"## Inventory",
|
|
277
|
+
f"",
|
|
278
|
+
f"| Category | Count |",
|
|
279
|
+
f"|----------|-------|",
|
|
280
|
+
f"| Assets | {model.asset_count} |",
|
|
281
|
+
f"| Trust Boundaries | {model.boundary_count} |",
|
|
282
|
+
f"| Threat Actors | {model.actor_count} |",
|
|
283
|
+
f"| Identified Threats | {model.threat_count} |",
|
|
284
|
+
f"| Attack Scenarios | {model.scenario_count} |",
|
|
285
|
+
f"",
|
|
286
|
+
f"## Severity Breakdown",
|
|
287
|
+
f"",
|
|
288
|
+
f"| Severity | Count |",
|
|
289
|
+
f"|----------|-------|",
|
|
290
|
+
]
|
|
291
|
+
for sev in ThreatSeverity:
|
|
292
|
+
count = len(result.threats_by_severity.get(sev.value, []))
|
|
293
|
+
lines.append(f"| {sev.value} | {count} |")
|
|
294
|
+
|
|
295
|
+
lines += [
|
|
296
|
+
f"",
|
|
297
|
+
f"## Control Domain Coverage",
|
|
298
|
+
f"",
|
|
299
|
+
f"| Domain | Covered |",
|
|
300
|
+
f"|--------|---------|",
|
|
301
|
+
]
|
|
302
|
+
for domain, covered in result.control_coverage.items():
|
|
303
|
+
mark = "✓" if covered else "✗"
|
|
304
|
+
lines.append(f"| {domain} | {mark} |")
|
|
305
|
+
|
|
306
|
+
lines += [
|
|
307
|
+
f"",
|
|
308
|
+
f"## Threats",
|
|
309
|
+
f"",
|
|
310
|
+
]
|
|
311
|
+
for sev in ThreatSeverity:
|
|
312
|
+
threats = result.threats_by_severity.get(sev.value, [])
|
|
313
|
+
if threats:
|
|
314
|
+
lines.append(f"### {sev.value}")
|
|
315
|
+
lines.append(f"")
|
|
316
|
+
for t in threats:
|
|
317
|
+
lines.append(f"#### {t['name']}")
|
|
318
|
+
lines.append(f"")
|
|
319
|
+
lines.append(f"- **ID:** `{t['threat_id']}`")
|
|
320
|
+
lines.append(f"- **Category:** {t['category']}")
|
|
321
|
+
lines.append(f"- **Controls:** {', '.join(t.get('actshield_controls', []))}")
|
|
322
|
+
lines.append(f"- **Effectiveness:** {t.get('control_effectiveness', 'N/A')}")
|
|
323
|
+
lines.append(f"- **Residual Risk:** {t.get('residual_risk', 'N/A')}")
|
|
324
|
+
lines.append(f"")
|
|
325
|
+
|
|
326
|
+
if result.residual_risks:
|
|
327
|
+
lines += [f"## Residual Risks", f""]
|
|
328
|
+
for risk in result.residual_risks:
|
|
329
|
+
lines.append(
|
|
330
|
+
f"- **{risk['name']}** ({risk['severity']}) — "
|
|
331
|
+
f"residual score: {risk['residual_score']:.1f} — {risk['residual_risk']}"
|
|
332
|
+
)
|
|
333
|
+
lines.append(f"")
|
|
334
|
+
|
|
335
|
+
lines += [
|
|
336
|
+
f"---",
|
|
337
|
+
f"",
|
|
338
|
+
f"*Generated by ActShield v0.9.0*",
|
|
339
|
+
f"",
|
|
340
|
+
]
|
|
341
|
+
return "\n".join(lines)
|