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,186 @@
|
|
|
1
|
+
"""Authority graph — builds and queries the agent authority/delegation graph.
|
|
2
|
+
|
|
3
|
+
Read-only. Does not grant or modify any authority.
|
|
4
|
+
Uses data already present in delegation_registry and agent_registry.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Dict, List, Optional, Set
|
|
10
|
+
from actshield.forensics.models import AuthorityRelationship, DelegationRelationship
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from actshield.client import ActShield
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuthorityGraph:
|
|
17
|
+
"""Directed authority graph built from the ActShield delegation registry.
|
|
18
|
+
|
|
19
|
+
Nodes = agent_ids
|
|
20
|
+
Edges = delegation records (delegator → delegate, labelled with capabilities)
|
|
21
|
+
|
|
22
|
+
Never grants authority — read-only projection of existing security state.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
# adjacency: delegator_id → list of DelegationRelationship
|
|
27
|
+
self._outgoing: Dict[str, List[DelegationRelationship]] = {}
|
|
28
|
+
# reverse: delegate_id → list of DelegationRelationship
|
|
29
|
+
self._incoming: Dict[str, List[DelegationRelationship]] = {}
|
|
30
|
+
|
|
31
|
+
# ------------------------------------------------------------------
|
|
32
|
+
# Build
|
|
33
|
+
# ------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_guard(cls, guard: "ActShield") -> "AuthorityGraph":
|
|
37
|
+
"""Construct graph from an ActShield instance's delegation registry."""
|
|
38
|
+
g = cls()
|
|
39
|
+
for dlg in guard.delegation_registry.values():
|
|
40
|
+
rel = DelegationRelationship(
|
|
41
|
+
delegation_id=dlg.delegation_id,
|
|
42
|
+
delegator_agent_id=dlg.delegator_agent_id,
|
|
43
|
+
delegate_agent_id=dlg.delegate_agent_id,
|
|
44
|
+
granted_capabilities=list(dlg.authority_grant.granted_capabilities),
|
|
45
|
+
depth=dlg.depth,
|
|
46
|
+
parent_delegation_id=dlg.parent_delegation_id,
|
|
47
|
+
task_id=dlg.task_id,
|
|
48
|
+
trace_id=dlg.trace_id,
|
|
49
|
+
status=dlg.status,
|
|
50
|
+
timestamp=dlg.created_at,
|
|
51
|
+
)
|
|
52
|
+
g._add_edge(rel)
|
|
53
|
+
return g
|
|
54
|
+
|
|
55
|
+
def add_delegation(self, rel: DelegationRelationship) -> None:
|
|
56
|
+
"""Add a delegation relationship to the graph."""
|
|
57
|
+
self._add_edge(rel)
|
|
58
|
+
|
|
59
|
+
def _add_edge(self, rel: DelegationRelationship) -> None:
|
|
60
|
+
src = rel.delegator_agent_id
|
|
61
|
+
dst = rel.delegate_agent_id
|
|
62
|
+
self._outgoing.setdefault(src, []).append(rel)
|
|
63
|
+
self._incoming.setdefault(dst, []).append(rel)
|
|
64
|
+
|
|
65
|
+
# ------------------------------------------------------------------
|
|
66
|
+
# Queries
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
def get_delegations_from(self, agent_id: str) -> List[DelegationRelationship]:
|
|
70
|
+
"""Return all delegations initiated by agent_id."""
|
|
71
|
+
return list(self._outgoing.get(agent_id, []))
|
|
72
|
+
|
|
73
|
+
def get_delegations_to(self, agent_id: str) -> List[DelegationRelationship]:
|
|
74
|
+
"""Return all delegations received by agent_id."""
|
|
75
|
+
return list(self._incoming.get(agent_id, []))
|
|
76
|
+
|
|
77
|
+
def get_authority_relationships(
|
|
78
|
+
self, agent_id: str
|
|
79
|
+
) -> List[AuthorityRelationship]:
|
|
80
|
+
"""Return AuthorityRelationship objects where agent_id is delegator or delegate."""
|
|
81
|
+
rels: List[AuthorityRelationship] = []
|
|
82
|
+
for dlg in self.get_delegations_from(agent_id):
|
|
83
|
+
rels.append(
|
|
84
|
+
AuthorityRelationship(
|
|
85
|
+
source_agent_id=dlg.delegator_agent_id,
|
|
86
|
+
target_agent_id=dlg.delegate_agent_id,
|
|
87
|
+
capabilities=dlg.granted_capabilities,
|
|
88
|
+
delegation_id=dlg.delegation_id,
|
|
89
|
+
task_id=dlg.task_id,
|
|
90
|
+
trace_id=dlg.trace_id,
|
|
91
|
+
depth=dlg.depth,
|
|
92
|
+
timestamp=dlg.timestamp,
|
|
93
|
+
status=dlg.status,
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
for dlg in self.get_delegations_to(agent_id):
|
|
97
|
+
rels.append(
|
|
98
|
+
AuthorityRelationship(
|
|
99
|
+
source_agent_id=dlg.delegator_agent_id,
|
|
100
|
+
target_agent_id=dlg.delegate_agent_id,
|
|
101
|
+
capabilities=dlg.granted_capabilities,
|
|
102
|
+
delegation_id=dlg.delegation_id,
|
|
103
|
+
task_id=dlg.task_id,
|
|
104
|
+
trace_id=dlg.trace_id,
|
|
105
|
+
depth=dlg.depth,
|
|
106
|
+
timestamp=dlg.timestamp,
|
|
107
|
+
status=dlg.status,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
return rels
|
|
111
|
+
|
|
112
|
+
def get_delegation_chain(self, agent_id: str) -> List[DelegationRelationship]:
|
|
113
|
+
"""Walk the delegation chain from root to agent_id (recursive)."""
|
|
114
|
+
chain: List[DelegationRelationship] = []
|
|
115
|
+
visited: Set[str] = set()
|
|
116
|
+
self._walk_up(agent_id, chain, visited)
|
|
117
|
+
chain.sort(key=lambda r: r.depth)
|
|
118
|
+
return chain
|
|
119
|
+
|
|
120
|
+
def _walk_up(
|
|
121
|
+
self,
|
|
122
|
+
agent_id: str,
|
|
123
|
+
chain: List[DelegationRelationship],
|
|
124
|
+
visited: Set[str],
|
|
125
|
+
) -> None:
|
|
126
|
+
if agent_id in visited:
|
|
127
|
+
return
|
|
128
|
+
visited.add(agent_id)
|
|
129
|
+
for rel in self.get_delegations_to(agent_id):
|
|
130
|
+
chain.append(rel)
|
|
131
|
+
self._walk_up(rel.delegator_agent_id, chain, visited)
|
|
132
|
+
|
|
133
|
+
def get_all_delegatees(self, agent_id: str) -> List[str]:
|
|
134
|
+
"""Return all agents that agent_id has (directly or transitively) delegated to."""
|
|
135
|
+
result: List[str] = []
|
|
136
|
+
visited: Set[str] = set()
|
|
137
|
+
self._walk_down(agent_id, result, visited)
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
def _walk_down(
|
|
141
|
+
self, agent_id: str, result: List[str], visited: Set[str]
|
|
142
|
+
) -> None:
|
|
143
|
+
if agent_id in visited:
|
|
144
|
+
return
|
|
145
|
+
visited.add(agent_id)
|
|
146
|
+
for rel in self.get_delegations_from(agent_id):
|
|
147
|
+
dst = rel.delegate_agent_id
|
|
148
|
+
if dst not in result:
|
|
149
|
+
result.append(dst)
|
|
150
|
+
self._walk_down(dst, result, visited)
|
|
151
|
+
|
|
152
|
+
def get_delegated_capabilities(self, agent_id: str) -> List[str]:
|
|
153
|
+
"""Return all capabilities delegated TO agent_id (most recent active delegation)."""
|
|
154
|
+
delegations = self.get_delegations_to(agent_id)
|
|
155
|
+
if not delegations:
|
|
156
|
+
return []
|
|
157
|
+
# Prefer active ones, fall back to completed
|
|
158
|
+
active = [d for d in delegations if d.status == "active"]
|
|
159
|
+
source = active if active else delegations
|
|
160
|
+
caps: Set[str] = set()
|
|
161
|
+
for d in source:
|
|
162
|
+
caps.update(d.granted_capabilities)
|
|
163
|
+
return sorted(caps)
|
|
164
|
+
|
|
165
|
+
def get_all_agents(self) -> List[str]:
|
|
166
|
+
"""Return all agent IDs present in the graph."""
|
|
167
|
+
agents: Set[str] = set()
|
|
168
|
+
agents.update(self._outgoing.keys())
|
|
169
|
+
agents.update(self._incoming.keys())
|
|
170
|
+
return sorted(agents)
|
|
171
|
+
|
|
172
|
+
def to_dict(self) -> dict:
|
|
173
|
+
"""Serializable representation of the authority graph."""
|
|
174
|
+
edges = []
|
|
175
|
+
seen: Set[str] = set()
|
|
176
|
+
for rels in self._outgoing.values():
|
|
177
|
+
for rel in rels:
|
|
178
|
+
if rel.delegation_id not in seen:
|
|
179
|
+
seen.add(rel.delegation_id)
|
|
180
|
+
edges.append(rel.model_dump(mode="json"))
|
|
181
|
+
return {
|
|
182
|
+
"nodes": self.get_all_agents(),
|
|
183
|
+
"edges": edges,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Effective access calculator — deterministic, never uses AI.
|
|
2
|
+
|
|
3
|
+
Calculates:
|
|
4
|
+
effective_access = declared_capabilities
|
|
5
|
+
+ valid_delegated_capabilities
|
|
6
|
+
- explicit_restrictions
|
|
7
|
+
|
|
8
|
+
Uses existing ActShield:
|
|
9
|
+
- agent_registry (declared capabilities)
|
|
10
|
+
- delegation_registry (delegated capabilities)
|
|
11
|
+
- tool_registry (required capabilities per tool)
|
|
12
|
+
- authority containment logic
|
|
13
|
+
|
|
14
|
+
Does NOT use AI Secura or APIRIS for access determination.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import TYPE_CHECKING, Dict, List, Set
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from actshield.client import ActShield
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EffectiveAccessCalculator:
|
|
26
|
+
"""Deterministic calculation of effective capabilities and reachable resources.
|
|
27
|
+
|
|
28
|
+
effective_access = declared_capabilities
|
|
29
|
+
+ valid_delegated_capabilities
|
|
30
|
+
- explicit_restrictions
|
|
31
|
+
|
|
32
|
+
Monotonic authority containment is preserved — a delegated agent can
|
|
33
|
+
never receive more authority than the delegator holds.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, guard: "ActShield") -> None:
|
|
37
|
+
self._guard = guard
|
|
38
|
+
|
|
39
|
+
def get_effective_capabilities(self, agent_id: str) -> List[str]:
|
|
40
|
+
"""Return the union of declared + delegated capabilities for agent_id."""
|
|
41
|
+
agent = self._guard.agent_registry.get(agent_id)
|
|
42
|
+
declared: Set[str] = set(agent.capability_names()) if agent else set()
|
|
43
|
+
|
|
44
|
+
delegated: Set[str] = set()
|
|
45
|
+
for dlg in self._guard.delegation_registry.values():
|
|
46
|
+
if dlg.delegate_agent_id == agent_id and dlg.status in ("active", "completed"):
|
|
47
|
+
delegated.update(dlg.authority_grant.granted_capabilities)
|
|
48
|
+
|
|
49
|
+
return sorted(declared | delegated)
|
|
50
|
+
|
|
51
|
+
def get_declared_capabilities(self, agent_id: str) -> List[str]:
|
|
52
|
+
agent = self._guard.agent_registry.get(agent_id)
|
|
53
|
+
return agent.capability_names() if agent else []
|
|
54
|
+
|
|
55
|
+
def get_delegated_capabilities(self, agent_id: str) -> List[str]:
|
|
56
|
+
caps: Set[str] = set()
|
|
57
|
+
for dlg in self._guard.delegation_registry.values():
|
|
58
|
+
if dlg.delegate_agent_id == agent_id and dlg.status in ("active", "completed"):
|
|
59
|
+
caps.update(dlg.authority_grant.granted_capabilities)
|
|
60
|
+
return sorted(caps)
|
|
61
|
+
|
|
62
|
+
def get_reachable_tools(self, agent_id: str) -> List[str]:
|
|
63
|
+
"""Return tools this agent can invoke based on effective capabilities."""
|
|
64
|
+
effective = set(self.get_effective_capabilities(agent_id))
|
|
65
|
+
reachable = []
|
|
66
|
+
for tool_def in self._guard.tool_registry.values():
|
|
67
|
+
required = set(tool_def.required_capabilities)
|
|
68
|
+
if not required or required.issubset(effective):
|
|
69
|
+
reachable.append(tool_def.name)
|
|
70
|
+
return sorted(reachable)
|
|
71
|
+
|
|
72
|
+
def get_reachable_resources(self, agent_id: str) -> List[str]:
|
|
73
|
+
"""Return resources this agent can reach via effective capabilities."""
|
|
74
|
+
reachable_tools = set(self.get_reachable_tools(agent_id))
|
|
75
|
+
resources: List[str] = []
|
|
76
|
+
seen: Set[str] = set()
|
|
77
|
+
for tool_def in self._guard.tool_registry.values():
|
|
78
|
+
if tool_def.name in reachable_tools:
|
|
79
|
+
for resource in tool_def.target_resources:
|
|
80
|
+
if resource.name not in seen:
|
|
81
|
+
seen.add(resource.name)
|
|
82
|
+
resources.append(resource.name)
|
|
83
|
+
return sorted(resources)
|
|
84
|
+
|
|
85
|
+
def can_access_tool(self, agent_id: str, tool_name: str) -> bool:
|
|
86
|
+
"""Check if agent has effective permission to invoke a named tool."""
|
|
87
|
+
tool_def = self._get_tool(tool_name)
|
|
88
|
+
if tool_def is None:
|
|
89
|
+
return False
|
|
90
|
+
effective = set(self.get_effective_capabilities(agent_id))
|
|
91
|
+
required = set(tool_def.required_capabilities)
|
|
92
|
+
return not required or required.issubset(effective)
|
|
93
|
+
|
|
94
|
+
def can_access_resource(self, agent_id: str, resource_name: str) -> bool:
|
|
95
|
+
"""Check if agent has effective permission to reach a named resource."""
|
|
96
|
+
return resource_name in self.get_reachable_resources(agent_id)
|
|
97
|
+
|
|
98
|
+
def build_access_matrix(
|
|
99
|
+
self,
|
|
100
|
+
agent_ids: List[str],
|
|
101
|
+
resource_names: List[str],
|
|
102
|
+
) -> Dict[str, Dict[str, bool]]:
|
|
103
|
+
"""Build effective-permission matrix: agent × resource → bool."""
|
|
104
|
+
matrix: Dict[str, Dict[str, bool]] = {}
|
|
105
|
+
for agent_id in agent_ids:
|
|
106
|
+
matrix[agent_id] = {}
|
|
107
|
+
for resource_name in resource_names:
|
|
108
|
+
matrix[agent_id][resource_name] = self.can_access_resource(
|
|
109
|
+
agent_id, resource_name
|
|
110
|
+
)
|
|
111
|
+
return matrix
|
|
112
|
+
|
|
113
|
+
def _get_tool(self, tool_name: str):
|
|
114
|
+
for tool_def in self._guard.tool_registry.values():
|
|
115
|
+
if tool_def.name == tool_name:
|
|
116
|
+
return tool_def
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Deterministic forensic explainer — assembles evidence-backed explanations.
|
|
2
|
+
|
|
3
|
+
The explanation is assembled from ACTUAL recorded evidence.
|
|
4
|
+
No LLM is called. No AI invents the explanation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from actshield.forensics.models import (
|
|
12
|
+
AccessDecision,
|
|
13
|
+
AttackForensicReport,
|
|
14
|
+
ForensicExplanation,
|
|
15
|
+
ForensicIncident,
|
|
16
|
+
IncidentSeverity,
|
|
17
|
+
IncidentStatus,
|
|
18
|
+
IncidentType,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from actshield.forensics.service import ForensicService
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ForensicExplainer:
|
|
26
|
+
"""Builds deterministic forensic explanations from recorded evidence.
|
|
27
|
+
|
|
28
|
+
Source of truth chain:
|
|
29
|
+
SecurityDecision → structured_explanation → authority_containment
|
|
30
|
+
→ intent_alignment → risk_breakdown + SecurityAnalysisEvidence
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, service: "ForensicService") -> None:
|
|
34
|
+
self._svc = service
|
|
35
|
+
|
|
36
|
+
def explain_decision(
|
|
37
|
+
self,
|
|
38
|
+
event_id: Optional[str] = None,
|
|
39
|
+
trace_id: Optional[str] = None,
|
|
40
|
+
agent_id: Optional[str] = None,
|
|
41
|
+
tool_name: Optional[str] = None,
|
|
42
|
+
decision_dict: Optional[Dict[str, Any]] = None,
|
|
43
|
+
) -> ForensicExplanation:
|
|
44
|
+
"""Build a forensic explanation from a stored security decision.
|
|
45
|
+
|
|
46
|
+
Falls back to the decision_dict if no storage record is found.
|
|
47
|
+
"""
|
|
48
|
+
resolved_decision: Dict[str, Any] = {}
|
|
49
|
+
|
|
50
|
+
# Try to pull from storage by trace_id
|
|
51
|
+
if trace_id and self._svc._guard.storage:
|
|
52
|
+
stored = self._svc._guard.storage.get_decisions(trace_id=trace_id, limit=1)
|
|
53
|
+
if stored:
|
|
54
|
+
resolved_decision = stored[0]
|
|
55
|
+
|
|
56
|
+
# Merge with provided dict
|
|
57
|
+
if decision_dict:
|
|
58
|
+
resolved_decision.update(decision_dict)
|
|
59
|
+
|
|
60
|
+
structured = resolved_decision.get("details_json") or {}
|
|
61
|
+
if isinstance(structured, str):
|
|
62
|
+
import json
|
|
63
|
+
try:
|
|
64
|
+
structured = json.loads(structured)
|
|
65
|
+
except Exception:
|
|
66
|
+
structured = {}
|
|
67
|
+
|
|
68
|
+
se = structured.get("structured_explanation", {})
|
|
69
|
+
auth = structured.get("authority_containment", {})
|
|
70
|
+
intent = structured.get("intent_alignment", {})
|
|
71
|
+
|
|
72
|
+
# Map decision action to AccessDecision
|
|
73
|
+
action_str = resolved_decision.get("action", "UNKNOWN").upper()
|
|
74
|
+
try:
|
|
75
|
+
policy_decision = AccessDecision(action_str)
|
|
76
|
+
except ValueError:
|
|
77
|
+
policy_decision = AccessDecision.UNKNOWN
|
|
78
|
+
|
|
79
|
+
# Influencing context — from the access graph
|
|
80
|
+
attempts = self._svc.access_graph.get_agent_attempts(agent_id or "")
|
|
81
|
+
related = [a for a in attempts if a.tool_name == (tool_name or "")]
|
|
82
|
+
context_sources = []
|
|
83
|
+
context_ids = []
|
|
84
|
+
taint_state = "CLEAN"
|
|
85
|
+
if related:
|
|
86
|
+
latest = related[-1]
|
|
87
|
+
context_ids = latest.context_ids
|
|
88
|
+
taint_state = latest.taint_state
|
|
89
|
+
|
|
90
|
+
# Context sources from structured explanation
|
|
91
|
+
context_sources = se.get("context_sources", [])
|
|
92
|
+
|
|
93
|
+
# Delegated authority
|
|
94
|
+
delegated_caps = se.get("delegated_capabilities", [])
|
|
95
|
+
if isinstance(delegated_caps, str):
|
|
96
|
+
delegated_caps = [delegated_caps] if delegated_caps else []
|
|
97
|
+
|
|
98
|
+
effective_authority = "GRANTED" if auth.get("contained", False) else "DENIED"
|
|
99
|
+
|
|
100
|
+
# AI/APIRIS summaries
|
|
101
|
+
ai_summary = None
|
|
102
|
+
apiris_summary = None
|
|
103
|
+
if agent_id:
|
|
104
|
+
for attempt in self._svc.access_graph.get_agent_attempts(agent_id):
|
|
105
|
+
if attempt.tool_name == (tool_name or ""):
|
|
106
|
+
# Check access graph for security evidence references
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
return ForensicExplanation(
|
|
110
|
+
event_id=event_id,
|
|
111
|
+
trace_id=trace_id or resolved_decision.get("trace_id"),
|
|
112
|
+
agent_id=agent_id or se.get("agent", ""),
|
|
113
|
+
tool_name=tool_name or se.get("requested_tool", ""),
|
|
114
|
+
resource_name=se.get("sensitive_sink"),
|
|
115
|
+
resource_sensitivity=se.get("sink_sensitivity", "UNKNOWN"),
|
|
116
|
+
original_intent=intent.get("original_intent", "Unspecified"),
|
|
117
|
+
influencing_context_sources=context_sources,
|
|
118
|
+
influencing_context_ids=context_ids,
|
|
119
|
+
context_trust="UNTRUSTED" if taint_state == "TAINTED" else "TRUSTED",
|
|
120
|
+
taint_state=taint_state,
|
|
121
|
+
delegated_authority=delegated_caps,
|
|
122
|
+
requested_capability=(
|
|
123
|
+
se.get("required_capabilities", [None])[0]
|
|
124
|
+
if isinstance(se.get("required_capabilities"), list)
|
|
125
|
+
else se.get("required_capabilities")
|
|
126
|
+
),
|
|
127
|
+
effective_authority=effective_authority,
|
|
128
|
+
policy_decision=policy_decision,
|
|
129
|
+
policy_reason_code=resolved_decision.get("reason_code", ""),
|
|
130
|
+
policy_explanation=resolved_decision.get("explanation", ""),
|
|
131
|
+
ai_secura_summary=ai_summary,
|
|
132
|
+
apiris_summary=apiris_summary,
|
|
133
|
+
tool_executed=False,
|
|
134
|
+
execution_count=0,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def explain_attack(
|
|
138
|
+
self, attack_id: str
|
|
139
|
+
) -> Optional[AttackForensicReport]:
|
|
140
|
+
"""Build an AttackForensicReport from Phase 5 evidence."""
|
|
141
|
+
result = self._svc._attack_results.get(attack_id)
|
|
142
|
+
if result is None:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
ev = result.security_evidence
|
|
146
|
+
lineage = result.mutation_lineage
|
|
147
|
+
|
|
148
|
+
# Reachable resources for acting agent
|
|
149
|
+
agent_id = ev.acting_agent if ev else ""
|
|
150
|
+
reachable = []
|
|
151
|
+
if agent_id and self._svc._calculator:
|
|
152
|
+
reachable = self._svc._calculator.get_reachable_resources(agent_id)
|
|
153
|
+
|
|
154
|
+
ai_summary = None
|
|
155
|
+
apiris_summary = None
|
|
156
|
+
if ev:
|
|
157
|
+
if ev.ai_analysis and not ev.ai_unavailable:
|
|
158
|
+
ai_summary = str(
|
|
159
|
+
ev.ai_analysis.get("threat_classification") or
|
|
160
|
+
ev.ai_analysis.get("security_level") or ""
|
|
161
|
+
)
|
|
162
|
+
if ev.apiris_analysis and not ev.apiris_unavailable:
|
|
163
|
+
apiris_summary = str(
|
|
164
|
+
ev.apiris_analysis.get("risk_level") or
|
|
165
|
+
ev.apiris_analysis.get("classification") or ""
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
policy_dec = AccessDecision(result.actual_decision.upper())
|
|
170
|
+
except ValueError:
|
|
171
|
+
policy_dec = AccessDecision.UNKNOWN
|
|
172
|
+
|
|
173
|
+
return AttackForensicReport(
|
|
174
|
+
attack_id=attack_id,
|
|
175
|
+
campaign_id=result.campaign_id,
|
|
176
|
+
attack_type=result.attack_type.value,
|
|
177
|
+
target=result.target,
|
|
178
|
+
entry_point=result.entry_point,
|
|
179
|
+
agent_id=agent_id,
|
|
180
|
+
trace_id=result.trace_id,
|
|
181
|
+
original_intent=ev.task_intent if ev else "",
|
|
182
|
+
mutation_lineage=lineage,
|
|
183
|
+
adaptive_iteration=getattr(result, "adaptive_iteration", 0),
|
|
184
|
+
influencing_contexts=ev.context_ids if ev else [],
|
|
185
|
+
context_trust="UNTRUSTED" if (ev and ev.taint_state == "TAINTED") else "TRUSTED",
|
|
186
|
+
taint_state=ev.taint_state if ev else "UNKNOWN",
|
|
187
|
+
delegated_capabilities=ev.delegated_capabilities if ev else [],
|
|
188
|
+
requested_capability=ev.requested_capability if ev else None,
|
|
189
|
+
effective_capabilities=ev.delegated_capabilities if ev else [],
|
|
190
|
+
authority_contained=ev.authority_contained if ev else True,
|
|
191
|
+
resource_name=None,
|
|
192
|
+
apiris_summary=apiris_summary,
|
|
193
|
+
ai_secura_summary=ai_summary,
|
|
194
|
+
risk_score=ev.risk_score if ev else 0.0,
|
|
195
|
+
policy_decision=policy_dec,
|
|
196
|
+
policy_reason_code=ev.policy_reason_code if ev else "",
|
|
197
|
+
policy_explanation=ev.policy_explanation if ev else "",
|
|
198
|
+
tool_executed=result.actual_execution,
|
|
199
|
+
execution_count=result.execution_evidence.execution_count,
|
|
200
|
+
sensitive_db_calls=result.execution_evidence.sensitive_db_calls,
|
|
201
|
+
actual_access=result.actual_execution,
|
|
202
|
+
reachable_resources=reachable,
|
|
203
|
+
bypassed=result.bypassed,
|
|
204
|
+
expected_decision=result.expected_decision,
|
|
205
|
+
actual_decision=result.actual_decision,
|
|
206
|
+
regression_status=(
|
|
207
|
+
"REGRESSION_CREATED" if result.bypassed else "NONE"
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def classify_incident(
|
|
212
|
+
self, attempt: "ForensicExplanation"
|
|
213
|
+
) -> IncidentType:
|
|
214
|
+
"""Classify incident type deterministically from the explanation."""
|
|
215
|
+
if attempt.bypass_detected:
|
|
216
|
+
return IncidentType.BYPASS_DETECTED
|
|
217
|
+
reason = attempt.policy_reason_code
|
|
218
|
+
if "TAINTED" in reason:
|
|
219
|
+
return IncidentType.TAINT_PROPAGATION
|
|
220
|
+
if "AUTHORITY" in reason or "DELEGATION" in reason:
|
|
221
|
+
return IncidentType.AUTHORITY_ESCALATION
|
|
222
|
+
if "CAPABILITY" in reason or "MISSING" in reason:
|
|
223
|
+
return IncidentType.CAPABILITY_VIOLATION
|
|
224
|
+
if "INTENT" in reason:
|
|
225
|
+
return IncidentType.INTENT_VIOLATION
|
|
226
|
+
if "UNTRUSTED" in reason:
|
|
227
|
+
return IncidentType.UNTRUSTED_SENSITIVE_ACCESS
|
|
228
|
+
return IncidentType.GENERIC
|
|
229
|
+
|
|
230
|
+
def build_incident(
|
|
231
|
+
self, explanation: ForensicExplanation
|
|
232
|
+
) -> ForensicIncident:
|
|
233
|
+
"""Build a ForensicIncident from an explanation (uses IDs, no duplication)."""
|
|
234
|
+
severity = (
|
|
235
|
+
IncidentSeverity.CRITICAL
|
|
236
|
+
if explanation.resource_sensitivity == "CRITICAL"
|
|
237
|
+
else IncidentSeverity.HIGH
|
|
238
|
+
if explanation.policy_decision == AccessDecision.BLOCK
|
|
239
|
+
else IncidentSeverity.MEDIUM
|
|
240
|
+
)
|
|
241
|
+
return ForensicIncident(
|
|
242
|
+
incident_type=self.classify_incident(explanation),
|
|
243
|
+
severity=severity,
|
|
244
|
+
agent_id=explanation.agent_id,
|
|
245
|
+
resource_name=explanation.resource_name,
|
|
246
|
+
tool_name=explanation.tool_name,
|
|
247
|
+
decision=explanation.policy_decision,
|
|
248
|
+
executed=explanation.tool_executed,
|
|
249
|
+
trace_id=explanation.trace_id,
|
|
250
|
+
attack_id=explanation.bypass_attack_id,
|
|
251
|
+
status=IncidentStatus.CONTAINED,
|
|
252
|
+
description=explanation.policy_explanation,
|
|
253
|
+
)
|
|
254
|
+
|