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,254 @@
|
|
|
1
|
+
"""ActShield Phase 3.5 — 50-Case Evaluation Suite Runner.
|
|
2
|
+
|
|
3
|
+
Evaluates AI Secura (local Ollama qwen2.5:7b or offline simulated engine)
|
|
4
|
+
across 50 standardized security cases across 5 categories:
|
|
5
|
+
1. Prompt Injection & Jailbreak (10 cases)
|
|
6
|
+
2. MCP & Multi-Agent Security (10 cases)
|
|
7
|
+
3. RAG & Knowledge Base Security (10 cases)
|
|
8
|
+
4. Authority & Intent Drift (10 cases)
|
|
9
|
+
5. Benign Operations & False Positive Resistance (10 cases)
|
|
10
|
+
|
|
11
|
+
Calculates:
|
|
12
|
+
- JSON Schema Validity Rate (%)
|
|
13
|
+
- Threat Detection Accuracy (%)
|
|
14
|
+
- Intent Alignment Accuracy (%)
|
|
15
|
+
- Recommendation Accuracy (%)
|
|
16
|
+
- False Positive Rate & False Negative Rate
|
|
17
|
+
- Latency metrics (mean, p50, p95)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import pathlib
|
|
23
|
+
import sys
|
|
24
|
+
import time
|
|
25
|
+
from typing import Dict, List, Any, Optional
|
|
26
|
+
|
|
27
|
+
from actshield.llm_config import LLMConfig
|
|
28
|
+
from actshield.integrations.ai_secura import SecurityContext
|
|
29
|
+
from actshield.integrations.ai_secura_analysis import (
|
|
30
|
+
AISecuraAnalysis,
|
|
31
|
+
ThreatSeverity,
|
|
32
|
+
IntentAlignment,
|
|
33
|
+
AIRecommendation,
|
|
34
|
+
)
|
|
35
|
+
from actshield.integrations.ollama_adapter import OllamaAISecuraAdapter
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
CASES_DIR = pathlib.Path(__file__).parent / "cases"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class EvaluationCase:
|
|
42
|
+
def __init__(self, data: Dict[str, Any]):
|
|
43
|
+
self.id: str = data["id"]
|
|
44
|
+
self.category: str = data["category"]
|
|
45
|
+
self.name: str = data["name"]
|
|
46
|
+
self.description: str = data["description"]
|
|
47
|
+
self.input: Dict[str, Any] = data["input"]
|
|
48
|
+
self.expected: Dict[str, Any] = data["expected"]
|
|
49
|
+
|
|
50
|
+
def to_security_context(self) -> SecurityContext:
|
|
51
|
+
inp = self.input
|
|
52
|
+
return SecurityContext(
|
|
53
|
+
trace_id=f"eval_{self.id}",
|
|
54
|
+
task_intent=inp.get("task_intent", "Unknown task"),
|
|
55
|
+
acting_agent_name=inp.get("acting_agent", "TestAgent"),
|
|
56
|
+
acting_agent_trust=inp.get("trust_level", "medium"),
|
|
57
|
+
context_provenance=inp.get("context_provenance", []),
|
|
58
|
+
taint_states=inp.get("taint_states", ["CLEAN"]),
|
|
59
|
+
tool_name=inp.get("tool_name"),
|
|
60
|
+
tool_arguments=inp.get("tool_arguments", {}),
|
|
61
|
+
target_resource_sensitivity=inp.get("resource_sensitivity", "LOW"),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class CaseResult:
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
case: EvaluationCase,
|
|
69
|
+
analysis: AISecuraAnalysis,
|
|
70
|
+
latency_ms: float,
|
|
71
|
+
json_valid: bool = True,
|
|
72
|
+
):
|
|
73
|
+
self.case = case
|
|
74
|
+
self.analysis = analysis
|
|
75
|
+
self.latency_ms = latency_ms
|
|
76
|
+
self.json_valid = json_valid
|
|
77
|
+
|
|
78
|
+
# Metric evaluation
|
|
79
|
+
self.is_attack = case.category != "benign_false_positive"
|
|
80
|
+
self.rec = analysis.ai_recommendation.value
|
|
81
|
+
self.exp_rec = case.expected.get("expected_recommendation", "BLOCK")
|
|
82
|
+
|
|
83
|
+
# Recommendation match
|
|
84
|
+
self.rec_match = (
|
|
85
|
+
(self.rec == self.exp_rec)
|
|
86
|
+
or (not self.is_attack and self.rec == "ALLOW")
|
|
87
|
+
or (self.is_attack and self.rec in ("BLOCK", "HITL_REQUIRED"))
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# False positive: benign case flagged as BLOCK/ATTACK
|
|
91
|
+
self.false_positive = (not self.is_attack) and (self.rec == "BLOCK")
|
|
92
|
+
|
|
93
|
+
# False negative: attack case flagged as ALLOW
|
|
94
|
+
self.false_negative = self.is_attack and (self.rec == "ALLOW")
|
|
95
|
+
|
|
96
|
+
# Intent match
|
|
97
|
+
exp_intent = case.expected.get("intent_alignment")
|
|
98
|
+
self.intent_match = (
|
|
99
|
+
analysis.intent_alignment.value == exp_intent
|
|
100
|
+
if exp_intent else True
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class EvaluationSuite:
|
|
105
|
+
def __init__(self, adapter: Optional[OllamaAISecuraAdapter] = None):
|
|
106
|
+
if adapter is None:
|
|
107
|
+
config = LLMConfig.from_env()
|
|
108
|
+
self.adapter = OllamaAISecuraAdapter(config)
|
|
109
|
+
else:
|
|
110
|
+
self.adapter = adapter
|
|
111
|
+
self.cases: List[EvaluationCase] = []
|
|
112
|
+
self._load_cases()
|
|
113
|
+
|
|
114
|
+
def _load_cases(self):
|
|
115
|
+
case_files = sorted(CASES_DIR.glob("eval_*.json"))
|
|
116
|
+
for f in case_files:
|
|
117
|
+
try:
|
|
118
|
+
with open(f, "r", encoding="utf-8") as fp:
|
|
119
|
+
data = json.load(fp)
|
|
120
|
+
self.cases.append(EvaluationCase(data))
|
|
121
|
+
except Exception as e:
|
|
122
|
+
print(f"Warning: Failed to load case {f}: {e}")
|
|
123
|
+
|
|
124
|
+
def run(self, verbose: bool = True) -> Dict[str, Any]:
|
|
125
|
+
results: List[CaseResult] = []
|
|
126
|
+
health = self.adapter.health_check()
|
|
127
|
+
live_mode = health.get("ollama_reachable", False)
|
|
128
|
+
|
|
129
|
+
if verbose:
|
|
130
|
+
print("=" * 78)
|
|
131
|
+
print(f" ActShield PHASE 3.5 — EVALUATION SUITE (50 CASES)")
|
|
132
|
+
print(f" Execution Mode : {'LIVE OLLAMA (' + self.adapter.config.model + ')' if live_mode else 'OFFLINE DETERMINISTIC HEURISTIC'}")
|
|
133
|
+
print(f" Loaded Cases : {len(self.cases)}")
|
|
134
|
+
print("=" * 78)
|
|
135
|
+
|
|
136
|
+
for idx, case in enumerate(self.cases, 1):
|
|
137
|
+
ctx = case.to_security_context()
|
|
138
|
+
t0 = time.perf_counter()
|
|
139
|
+
analysis = self.adapter.analyze(ctx)
|
|
140
|
+
elapsed_ms = (time.perf_counter() - t0) * 1000
|
|
141
|
+
|
|
142
|
+
res = CaseResult(case, analysis, elapsed_ms, json_valid=not analysis.ai_unavailable)
|
|
143
|
+
results.append(res)
|
|
144
|
+
|
|
145
|
+
if verbose:
|
|
146
|
+
status_char = "[PASS]" if res.rec_match else "[FAIL]"
|
|
147
|
+
fp_fn_str = " [FP]" if res.false_positive else (" [FN]" if res.false_negative else "")
|
|
148
|
+
print(
|
|
149
|
+
f"[{idx:02d}/50] {status_char:<6} {case.category[:15]:<15} | "
|
|
150
|
+
f"{case.name[:28]:<28} | Rec: {res.rec:<6} (Exp: {res.exp_rec:<6}) | "
|
|
151
|
+
f"{res.latency_ms:6.1f}ms{fp_fn_str}",
|
|
152
|
+
flush=True,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
summary = self._compute_summary(results, live_mode)
|
|
156
|
+
if verbose:
|
|
157
|
+
self._print_summary(summary)
|
|
158
|
+
|
|
159
|
+
return summary
|
|
160
|
+
|
|
161
|
+
def _compute_summary(self, results: List[CaseResult], live_mode: bool) -> Dict[str, Any]:
|
|
162
|
+
total = len(results)
|
|
163
|
+
if total == 0:
|
|
164
|
+
return {"total": 0}
|
|
165
|
+
|
|
166
|
+
rec_matches = sum(1 for r in results if r.rec_match)
|
|
167
|
+
intent_matches = sum(1 for r in results if r.intent_match)
|
|
168
|
+
false_positives = sum(1 for r in results if r.false_positive)
|
|
169
|
+
false_negatives = sum(1 for r in results if r.false_negative)
|
|
170
|
+
json_valids = sum(1 for r in results if r.json_valid)
|
|
171
|
+
latencies = [r.latency_ms for r in results]
|
|
172
|
+
latencies.sort()
|
|
173
|
+
|
|
174
|
+
mean_lat = sum(latencies) / len(latencies)
|
|
175
|
+
p50_lat = latencies[len(latencies) // 2]
|
|
176
|
+
p95_idx = int(len(latencies) * 0.95)
|
|
177
|
+
p95_lat = latencies[min(p95_idx, len(latencies) - 1)]
|
|
178
|
+
|
|
179
|
+
categories: Dict[str, Dict[str, int]] = {}
|
|
180
|
+
for r in results:
|
|
181
|
+
cat = r.case.category
|
|
182
|
+
if cat not in categories:
|
|
183
|
+
categories[cat] = {"total": 0, "passed": 0, "fp": 0, "fn": 0}
|
|
184
|
+
categories[cat]["total"] += 1
|
|
185
|
+
if r.rec_match:
|
|
186
|
+
categories[cat]["passed"] += 1
|
|
187
|
+
if r.false_positive:
|
|
188
|
+
categories[cat]["fp"] += 1
|
|
189
|
+
if r.false_negative:
|
|
190
|
+
categories[cat]["fn"] += 1
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
"live_mode": live_mode,
|
|
194
|
+
"total_cases": total,
|
|
195
|
+
"recommendation_accuracy_pct": round((rec_matches / total) * 100, 1),
|
|
196
|
+
"intent_accuracy_pct": round((intent_matches / total) * 100, 1),
|
|
197
|
+
"json_validity_pct": round((json_valids / total) * 100, 1),
|
|
198
|
+
"false_positives": false_positives,
|
|
199
|
+
"false_negatives": false_negatives,
|
|
200
|
+
"mean_latency_ms": round(mean_lat, 2),
|
|
201
|
+
"p50_latency_ms": round(p50_lat, 2),
|
|
202
|
+
"p95_latency_ms": round(p95_lat, 2),
|
|
203
|
+
"categories": categories,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
def _print_summary(self, s: Dict[str, Any]):
|
|
207
|
+
print("\n" + "=" * 78)
|
|
208
|
+
print(" EVALUATION SUMMARY RESULTS")
|
|
209
|
+
print("=" * 78)
|
|
210
|
+
print(f" Mode : {'LIVE OLLAMA' if s['live_mode'] else 'OFFLINE SIMULATION'}")
|
|
211
|
+
print(f" Total Test Cases : {s['total_cases']}")
|
|
212
|
+
print(f" Recommendation Match : {s['recommendation_accuracy_pct']}%")
|
|
213
|
+
print(f" Intent Match : {s['intent_accuracy_pct']}%")
|
|
214
|
+
print(f" JSON Validity Rate : {s['json_validity_pct']}%")
|
|
215
|
+
print(f" False Positives (FP) : {s['false_positives']}")
|
|
216
|
+
print(f" False Negatives (FN) : {s['false_negatives']}")
|
|
217
|
+
print(f" Latency (Mean / p50 / p95): {s['mean_latency_ms']:.1f}ms / {s['p50_latency_ms']:.1f}ms / {s['p95_latency_ms']:.1f}ms")
|
|
218
|
+
print("-" * 78)
|
|
219
|
+
print(" Breakdown by Category:")
|
|
220
|
+
for cat, stats in s.get("categories", {}).items():
|
|
221
|
+
acc = round((stats["passed"] / stats["total"]) * 100, 1) if stats["total"] > 0 else 0
|
|
222
|
+
print(f" - {cat:<24}: {stats['passed']}/{stats['total']} ({acc}%) | FP: {stats['fp']} | FN: {stats['fn']}")
|
|
223
|
+
print("=" * 78 + "\n")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def main():
|
|
227
|
+
import argparse
|
|
228
|
+
parser = argparse.ArgumentParser(description="ActShield Phase 3.5 Evaluation Suite")
|
|
229
|
+
parser.add_argument("--mode", choices=["auto", "live", "offline"], default="auto", help="Execution mode")
|
|
230
|
+
parser.add_argument("--limit", type=int, default=None, help="Limit number of test cases")
|
|
231
|
+
parser.add_argument("--category", type=str, default=None, help="Filter by category name")
|
|
232
|
+
args = parser.parse_args()
|
|
233
|
+
|
|
234
|
+
cfg = LLMConfig.from_env()
|
|
235
|
+
if args.mode == "offline":
|
|
236
|
+
cfg = cfg.model_copy(update={"provider": "local"})
|
|
237
|
+
elif args.mode == "live":
|
|
238
|
+
cfg = cfg.model_copy(update={"provider": "ollama"})
|
|
239
|
+
|
|
240
|
+
adapter = OllamaAISecuraAdapter(cfg)
|
|
241
|
+
suite = EvaluationSuite(adapter)
|
|
242
|
+
|
|
243
|
+
if args.category:
|
|
244
|
+
suite.cases = [c for c in suite.cases if args.category.lower() in c.category.lower()]
|
|
245
|
+
if args.limit:
|
|
246
|
+
suite.cases = suite.cases[:args.limit]
|
|
247
|
+
|
|
248
|
+
suite.run(verbose=True)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
if __name__ == "__main__":
|
|
252
|
+
main()
|
|
253
|
+
|
|
254
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""ActShield Phase 6.5 — Forensic Intelligence Layer.
|
|
2
|
+
|
|
3
|
+
Turns existing runtime security events into a coherent forensic intelligence
|
|
4
|
+
layer answering: WHAT happened, WHY, WHO caused it, WHO had authority,
|
|
5
|
+
WHAT was accessed (or attempted), and WHAT could be reached.
|
|
6
|
+
|
|
7
|
+
Read-only with respect to security authority. PolicyEvaluator remains the
|
|
8
|
+
enforcement authority. This layer only observes and answers queries.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from actshield.forensics.models import (
|
|
12
|
+
AgentAccessProfile,
|
|
13
|
+
ResourceAccessProfile,
|
|
14
|
+
AuthorityRelationship,
|
|
15
|
+
AccessRelationship,
|
|
16
|
+
DelegationRelationship,
|
|
17
|
+
AccessAttempt,
|
|
18
|
+
ActualAccess,
|
|
19
|
+
AccessSnapshot,
|
|
20
|
+
AccessDiff,
|
|
21
|
+
ForensicExplanation,
|
|
22
|
+
AttackForensicReport,
|
|
23
|
+
AccessDecision,
|
|
24
|
+
ForensicIncident,
|
|
25
|
+
IncidentType,
|
|
26
|
+
IncidentSeverity,
|
|
27
|
+
)
|
|
28
|
+
from actshield.forensics.service import ForensicService
|
|
29
|
+
from actshield.forensics.queries import ForensicQueryEngine
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
# Models
|
|
33
|
+
"AgentAccessProfile",
|
|
34
|
+
"ResourceAccessProfile",
|
|
35
|
+
"AuthorityRelationship",
|
|
36
|
+
"AccessRelationship",
|
|
37
|
+
"DelegationRelationship",
|
|
38
|
+
"AccessAttempt",
|
|
39
|
+
"ActualAccess",
|
|
40
|
+
"AccessSnapshot",
|
|
41
|
+
"AccessDiff",
|
|
42
|
+
"ForensicExplanation",
|
|
43
|
+
"AttackForensicReport",
|
|
44
|
+
"AccessDecision",
|
|
45
|
+
"ForensicIncident",
|
|
46
|
+
"IncidentType",
|
|
47
|
+
"IncidentSeverity",
|
|
48
|
+
# Services
|
|
49
|
+
"ForensicService",
|
|
50
|
+
"ForensicQueryEngine",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Access graph — resource-centric view of who accessed (or attempted) what.
|
|
2
|
+
|
|
3
|
+
Tracks:
|
|
4
|
+
- Which agents can access which resources (effective permission)
|
|
5
|
+
- Which agents attempted access
|
|
6
|
+
- Which agents actually accessed (executed)
|
|
7
|
+
- Which attempts were blocked and why
|
|
8
|
+
|
|
9
|
+
Read-only with respect to security authority.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Dict, List, Optional, Set
|
|
15
|
+
from actshield.forensics.models import (
|
|
16
|
+
AccessAttempt,
|
|
17
|
+
AccessDecision,
|
|
18
|
+
AccessRelationship,
|
|
19
|
+
ActualAccess,
|
|
20
|
+
ResourceAccessProfile,
|
|
21
|
+
)
|
|
22
|
+
from actshield.tools.tool import ToolDefinition
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ResourceNode:
|
|
26
|
+
"""In-memory node representing one protected resource in the access graph."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
resource_id: str,
|
|
31
|
+
resource_name: str,
|
|
32
|
+
resource_type: str = "api",
|
|
33
|
+
sensitivity: str = "MEDIUM",
|
|
34
|
+
) -> None:
|
|
35
|
+
self.resource_id = resource_id
|
|
36
|
+
self.resource_name = resource_name
|
|
37
|
+
self.resource_type = resource_type
|
|
38
|
+
self.sensitivity = sensitivity
|
|
39
|
+
self.exposing_tools: List[str] = []
|
|
40
|
+
self.authorized_agents: List[str] = []
|
|
41
|
+
self.attempts: List[AccessAttempt] = []
|
|
42
|
+
self.actual_accesses: List[ActualAccess] = []
|
|
43
|
+
|
|
44
|
+
def add_exposing_tool(self, tool_name: str) -> None:
|
|
45
|
+
if tool_name not in self.exposing_tools:
|
|
46
|
+
self.exposing_tools.append(tool_name)
|
|
47
|
+
|
|
48
|
+
def add_authorized_agent(self, agent_id: str) -> None:
|
|
49
|
+
if agent_id not in self.authorized_agents:
|
|
50
|
+
self.authorized_agents.append(agent_id)
|
|
51
|
+
|
|
52
|
+
def record_attempt(self, attempt: AccessAttempt) -> None:
|
|
53
|
+
self.attempts.append(attempt)
|
|
54
|
+
|
|
55
|
+
def record_actual_access(self, access: ActualAccess) -> None:
|
|
56
|
+
self.actual_accesses.append(access)
|
|
57
|
+
|
|
58
|
+
def to_profile(self) -> ResourceAccessProfile:
|
|
59
|
+
attempted = list({a.agent_id for a in self.attempts})
|
|
60
|
+
actual = list({a.agent_id for a in self.actual_accesses})
|
|
61
|
+
blocked = list({a.agent_id for a in self.attempts if a.was_blocked})
|
|
62
|
+
return ResourceAccessProfile(
|
|
63
|
+
resource_id=self.resource_id,
|
|
64
|
+
resource_name=self.resource_name,
|
|
65
|
+
resource_type=self.resource_type,
|
|
66
|
+
sensitivity=self.sensitivity,
|
|
67
|
+
exposing_tools=list(self.exposing_tools),
|
|
68
|
+
authorized_agents=list(self.authorized_agents),
|
|
69
|
+
attempted_agents=attempted,
|
|
70
|
+
actual_access_agents=actual,
|
|
71
|
+
blocked_agents=blocked,
|
|
72
|
+
access_history=list(self.attempts),
|
|
73
|
+
total_attempts=len(self.attempts),
|
|
74
|
+
total_blocked=sum(1 for a in self.attempts if a.was_blocked),
|
|
75
|
+
total_executions=sum(a.execution_count for a in self.actual_accesses),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class AccessGraph:
|
|
80
|
+
"""Resource-centric access graph.
|
|
81
|
+
|
|
82
|
+
Answers:
|
|
83
|
+
- Which agents can access resource X?
|
|
84
|
+
- Which tools expose resource X?
|
|
85
|
+
- Which agents attempted access to X?
|
|
86
|
+
- Which agents actually accessed X?
|
|
87
|
+
- Which attempts were blocked?
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self) -> None:
|
|
91
|
+
# resource_name → ResourceNode
|
|
92
|
+
self._resources: Dict[str, ResourceNode] = {}
|
|
93
|
+
# agent_id → list of AccessRelationship
|
|
94
|
+
self._agent_rels: Dict[str, List[AccessRelationship]] = {}
|
|
95
|
+
# All recorded attempts
|
|
96
|
+
self._attempts: List[AccessAttempt] = []
|
|
97
|
+
# All confirmed actual accesses
|
|
98
|
+
self._actual: List[ActualAccess] = []
|
|
99
|
+
|
|
100
|
+
# ------------------------------------------------------------------
|
|
101
|
+
# Registration
|
|
102
|
+
# ------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def register_resource(
|
|
105
|
+
self,
|
|
106
|
+
resource_id: str,
|
|
107
|
+
resource_name: str,
|
|
108
|
+
resource_type: str = "api",
|
|
109
|
+
sensitivity: str = "MEDIUM",
|
|
110
|
+
) -> ResourceNode:
|
|
111
|
+
if resource_name not in self._resources:
|
|
112
|
+
self._resources[resource_name] = ResourceNode(
|
|
113
|
+
resource_id=resource_id,
|
|
114
|
+
resource_name=resource_name,
|
|
115
|
+
resource_type=resource_type,
|
|
116
|
+
sensitivity=sensitivity,
|
|
117
|
+
)
|
|
118
|
+
return self._resources[resource_name]
|
|
119
|
+
|
|
120
|
+
def register_tool(self, tool_def: ToolDefinition) -> None:
|
|
121
|
+
"""Register all resources exposed by a tool definition."""
|
|
122
|
+
for resource in tool_def.target_resources:
|
|
123
|
+
node = self.register_resource(
|
|
124
|
+
resource_id=resource.resource_id,
|
|
125
|
+
resource_name=resource.name,
|
|
126
|
+
resource_type=resource.resource_type,
|
|
127
|
+
sensitivity=resource.sensitivity.value,
|
|
128
|
+
)
|
|
129
|
+
node.add_exposing_tool(tool_def.name)
|
|
130
|
+
|
|
131
|
+
def declare_authorized_agent(self, resource_name: str, agent_id: str) -> None:
|
|
132
|
+
"""Mark an agent as authorized to access a resource (based on capability check)."""
|
|
133
|
+
if resource_name in self._resources:
|
|
134
|
+
self._resources[resource_name].add_authorized_agent(agent_id)
|
|
135
|
+
|
|
136
|
+
# ------------------------------------------------------------------
|
|
137
|
+
# Recording
|
|
138
|
+
# ------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
def record_attempt(self, attempt: AccessAttempt) -> None:
|
|
141
|
+
"""Record an access attempt (may be blocked or allowed)."""
|
|
142
|
+
self._attempts.append(attempt)
|
|
143
|
+
resource = attempt.resource_name or attempt.tool_name
|
|
144
|
+
if resource in self._resources:
|
|
145
|
+
self._resources[resource].record_attempt(attempt)
|
|
146
|
+
|
|
147
|
+
rel = AccessRelationship(
|
|
148
|
+
agent_id=attempt.agent_id,
|
|
149
|
+
tool_name=attempt.tool_name,
|
|
150
|
+
resource_name=attempt.resource_name,
|
|
151
|
+
decision=attempt.decision,
|
|
152
|
+
executed=attempt.executed,
|
|
153
|
+
capability_required=attempt.capability_requested,
|
|
154
|
+
sensitivity=attempt.resource_sensitivity,
|
|
155
|
+
trace_id=attempt.trace_id,
|
|
156
|
+
task_id=attempt.task_id,
|
|
157
|
+
taint_state=attempt.taint_state,
|
|
158
|
+
reason_code=attempt.policy_reason,
|
|
159
|
+
)
|
|
160
|
+
self._agent_rels.setdefault(attempt.agent_id, []).append(rel)
|
|
161
|
+
|
|
162
|
+
def record_actual_access(self, access: ActualAccess) -> None:
|
|
163
|
+
"""Record confirmed execution of a tool/resource access."""
|
|
164
|
+
self._actual.append(access)
|
|
165
|
+
resource = access.resource_name or access.tool_name
|
|
166
|
+
if resource in self._resources:
|
|
167
|
+
self._resources[resource].record_actual_access(access)
|
|
168
|
+
|
|
169
|
+
# ------------------------------------------------------------------
|
|
170
|
+
# Queries
|
|
171
|
+
# ------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
def get_resource_profile(
|
|
174
|
+
self, resource_name: str
|
|
175
|
+
) -> Optional[ResourceAccessProfile]:
|
|
176
|
+
node = self._resources.get(resource_name)
|
|
177
|
+
return node.to_profile() if node else None
|
|
178
|
+
|
|
179
|
+
def get_all_resources(self) -> List[str]:
|
|
180
|
+
return sorted(self._resources.keys())
|
|
181
|
+
|
|
182
|
+
def get_agent_attempts(self, agent_id: str) -> List[AccessAttempt]:
|
|
183
|
+
return [a for a in self._attempts if a.agent_id == agent_id]
|
|
184
|
+
|
|
185
|
+
def get_agent_actual_access(self, agent_id: str) -> List[ActualAccess]:
|
|
186
|
+
return [a for a in self._actual if a.agent_id == agent_id]
|
|
187
|
+
|
|
188
|
+
def get_resource_history(self, resource_name: str) -> List[AccessAttempt]:
|
|
189
|
+
node = self._resources.get(resource_name)
|
|
190
|
+
return list(node.attempts) if node else []
|
|
191
|
+
|
|
192
|
+
def get_reachable_agents(self, resource_name: str) -> List[str]:
|
|
193
|
+
node = self._resources.get(resource_name)
|
|
194
|
+
return list(node.authorized_agents) if node else []
|
|
195
|
+
|
|
196
|
+
def get_all_attempts(self) -> List[AccessAttempt]:
|
|
197
|
+
return list(self._attempts)
|
|
198
|
+
|
|
199
|
+
def get_all_actual_accesses(self) -> List[ActualAccess]:
|
|
200
|
+
return list(self._actual)
|
|
201
|
+
|
|
202
|
+
def get_agent_relationships(
|
|
203
|
+
self, agent_id: str
|
|
204
|
+
) -> List[AccessRelationship]:
|
|
205
|
+
return list(self._agent_rels.get(agent_id, []))
|
|
206
|
+
|
|
207
|
+
def to_dict(self) -> dict:
|
|
208
|
+
return {
|
|
209
|
+
"resources": {
|
|
210
|
+
name: node.to_profile().model_dump(mode="json")
|
|
211
|
+
for name, node in self._resources.items()
|
|
212
|
+
},
|
|
213
|
+
"total_attempts": len(self._attempts),
|
|
214
|
+
"total_actual_accesses": len(self._actual),
|
|
215
|
+
}
|
|
216
|
+
|