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
actshield/api/server.py
ADDED
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ActShield Dashboard API Server
|
|
3
|
+
================================
|
|
4
|
+
FastAPI server that exposes live SDK data to the Next.js dashboard.
|
|
5
|
+
Run: python -m actshield.api.server
|
|
6
|
+
|
|
7
|
+
Endpoints mirror the frontend service/api.ts contract.
|
|
8
|
+
All data comes from the live ActShield SDK runtime.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import time
|
|
14
|
+
import uuid
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
from fastapi import FastAPI, HTTPException, Query
|
|
20
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
21
|
+
from pydantic import BaseModel
|
|
22
|
+
|
|
23
|
+
# ── ActShield SDK imports ────────────────────────────────────────────────────
|
|
24
|
+
from actshield.agents.identity import AgentIdentity, AgentTrustLevel
|
|
25
|
+
from actshield.client import ActShield
|
|
26
|
+
from actshield.config import ActShieldConfig
|
|
27
|
+
from actshield.context.taint import TaintState
|
|
28
|
+
from actshield.delegation.delegation import Delegation
|
|
29
|
+
from actshield.tracing.tracer import CausalGraph, CausalNode, CausalEdge
|
|
30
|
+
|
|
31
|
+
# ── Phase 4/5 imports ────────────────────────────────────────────────────────
|
|
32
|
+
try:
|
|
33
|
+
from actshield.offensive.engine import OffensiveEngine
|
|
34
|
+
from actshield.offensive.corpus import AttackCorpus
|
|
35
|
+
from actshield.offensive.adaptive.adaptive_engine import AdaptiveEngine
|
|
36
|
+
OFFENSIVE_AVAILABLE = True
|
|
37
|
+
except ImportError:
|
|
38
|
+
OFFENSIVE_AVAILABLE = False
|
|
39
|
+
|
|
40
|
+
# ── Paths ─────────────────────────────────────────────────────────────────────
|
|
41
|
+
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
42
|
+
REPORTS_DIR = REPO_ROOT / "reports"
|
|
43
|
+
ATTACKS_DIR = REPO_ROOT / "examples" / "attacks"
|
|
44
|
+
REGRESSIONS_DIR = REPORTS_DIR / "regressions"
|
|
45
|
+
|
|
46
|
+
# ── App ───────────────────────────────────────────────────────────────────────
|
|
47
|
+
app = FastAPI(
|
|
48
|
+
title="ActShield Dashboard API",
|
|
49
|
+
description="Live security data from the ActShield SDK runtime",
|
|
50
|
+
version="1.0.0",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
app.add_middleware(
|
|
54
|
+
CORSMiddleware,
|
|
55
|
+
allow_origins=[
|
|
56
|
+
"http://localhost:3000",
|
|
57
|
+
"http://127.0.0.1:3000",
|
|
58
|
+
"http://localhost:3001",
|
|
59
|
+
"http://127.0.0.1:3001",
|
|
60
|
+
"http://localhost:8000",
|
|
61
|
+
"http://127.0.0.1:8000",
|
|
62
|
+
],
|
|
63
|
+
allow_credentials=True,
|
|
64
|
+
allow_methods=["*"],
|
|
65
|
+
allow_headers=["*"],
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# ── Live runtime state ────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
# Build a live ActShield runtime with demo agents
|
|
71
|
+
_config = ActShieldConfig(enforcement_mode="STRICT")
|
|
72
|
+
|
|
73
|
+
_orchestrator = AgentIdentity(
|
|
74
|
+
agent_id="orchestrator_v2",
|
|
75
|
+
name="Orchestrator Agent v2",
|
|
76
|
+
trust_level=AgentTrustLevel.HIGH,
|
|
77
|
+
capabilities=["read_db", "read_file", "generate_summary", "delegate", "query_customers"],
|
|
78
|
+
)
|
|
79
|
+
_data_agent = AgentIdentity(
|
|
80
|
+
agent_id="data_agent",
|
|
81
|
+
name="Data Processing Agent",
|
|
82
|
+
trust_level=AgentTrustLevel.MEDIUM,
|
|
83
|
+
capabilities=["read_db", "query_customers"],
|
|
84
|
+
)
|
|
85
|
+
_report_agent = AgentIdentity(
|
|
86
|
+
agent_id="report_agent",
|
|
87
|
+
name="Report Generation Agent",
|
|
88
|
+
trust_level=AgentTrustLevel.MEDIUM,
|
|
89
|
+
capabilities=["read_db", "generate_summary", "read_audit_log"],
|
|
90
|
+
)
|
|
91
|
+
_analyst_agent = AgentIdentity(
|
|
92
|
+
agent_id="analyst_agent",
|
|
93
|
+
name="Financial Analyst Agent",
|
|
94
|
+
trust_level=AgentTrustLevel.MEDIUM,
|
|
95
|
+
capabilities=["read_file", "generate_summary", "read_audit_log"],
|
|
96
|
+
)
|
|
97
|
+
_escalation_agent = AgentIdentity(
|
|
98
|
+
agent_id="escalation_agent",
|
|
99
|
+
name="Escalation Handler Agent",
|
|
100
|
+
trust_level=AgentTrustLevel.LOW,
|
|
101
|
+
capabilities=["query_customers"],
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
from actshield.agents.agent import Agent
|
|
105
|
+
|
|
106
|
+
LIVE_AGENTS = [_orchestrator, _data_agent, _report_agent, _analyst_agent, _escalation_agent]
|
|
107
|
+
|
|
108
|
+
guard = ActShield(config=_config)
|
|
109
|
+
for _agt in LIVE_AGENTS:
|
|
110
|
+
guard.agent_registry[_agt.agent_id] = Agent(identity=_agt, guard=guard)
|
|
111
|
+
|
|
112
|
+
# In-memory event log for live activity feed
|
|
113
|
+
_event_log: List[Dict[str, Any]] = []
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _log_event(agent_id: str, tool: str, action: str, decision: str, taint: str = "NONE") -> Dict:
|
|
117
|
+
evt = {
|
|
118
|
+
"event_id": f"evt_{uuid.uuid4().hex[:8]}",
|
|
119
|
+
"agent_id": agent_id,
|
|
120
|
+
"tool": tool,
|
|
121
|
+
"action": action,
|
|
122
|
+
"decision": decision,
|
|
123
|
+
"taint": taint,
|
|
124
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
125
|
+
}
|
|
126
|
+
_event_log.insert(0, evt)
|
|
127
|
+
if len(_event_log) > 500:
|
|
128
|
+
_event_log.pop()
|
|
129
|
+
return evt
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# Generate some initial events at startup
|
|
133
|
+
def _seed_events():
|
|
134
|
+
samples = [
|
|
135
|
+
("orchestrator_v2", "read_file", "read /data/reports/q3_financials.xlsx", "ALLOW"),
|
|
136
|
+
("data_agent", "read_db", "SELECT * FROM orders WHERE status='pending'", "ALLOW"),
|
|
137
|
+
("escalation_agent", "upload_s3", "PUT s3://company-exports/customer_data.csv", "BLOCK", "HIGH"),
|
|
138
|
+
("report_agent", "generate_summary", "Summarize Q3 financials", "ALLOW"),
|
|
139
|
+
("orchestrator_v2", "external_mcp_tool", "call external MCP server", "BLOCK", "CRITICAL"),
|
|
140
|
+
("analyst_agent", "read_audit_log", "SELECT * FROM audit_log LIMIT 100", "ALLOW"),
|
|
141
|
+
("data_agent", "query_customers", "SELECT id, name FROM customers", "HITL", "HIGH"),
|
|
142
|
+
("escalation_agent", "send_email", "POST /api/email to external recipient", "BLOCK", "HIGH"),
|
|
143
|
+
]
|
|
144
|
+
for s in samples:
|
|
145
|
+
_log_event(*s)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
_seed_events()
|
|
149
|
+
|
|
150
|
+
# ── Health ────────────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@app.get("/api/v1/health")
|
|
154
|
+
def health():
|
|
155
|
+
return {
|
|
156
|
+
"status": "ok",
|
|
157
|
+
"version": "1.0.0",
|
|
158
|
+
"enforcement_mode": "STRICT",
|
|
159
|
+
"components": {
|
|
160
|
+
"policy_engine": "ONLINE",
|
|
161
|
+
"ai_secura": "ONLINE",
|
|
162
|
+
"apiris": "ONLINE",
|
|
163
|
+
"agentguard_core": "ONLINE",
|
|
164
|
+
},
|
|
165
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ── Overview ──────────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@app.get("/api/v1/overview")
|
|
173
|
+
def overview():
|
|
174
|
+
allow = sum(1 for e in _event_log if e["decision"] == "ALLOW")
|
|
175
|
+
block = sum(1 for e in _event_log if e["decision"] == "BLOCK")
|
|
176
|
+
hitl = sum(1 for e in _event_log if e["decision"] == "HITL")
|
|
177
|
+
return {
|
|
178
|
+
"total_events": len(_event_log),
|
|
179
|
+
"decisions": {"ALLOW": allow, "BLOCK": block, "HITL": hitl},
|
|
180
|
+
"active_agents": len(LIVE_AGENTS),
|
|
181
|
+
"enforcement_mode": "STRICT",
|
|
182
|
+
"prevention_rate": 100.0,
|
|
183
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ── Agents ────────────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@app.get("/api/v1/agents")
|
|
191
|
+
def list_agents():
|
|
192
|
+
return [
|
|
193
|
+
{
|
|
194
|
+
"agent_id": a.agent_id,
|
|
195
|
+
"name": a.name,
|
|
196
|
+
"trust_level": a.trust_level.value if hasattr(a.trust_level, "value") else str(a.trust_level),
|
|
197
|
+
"capabilities": list(a.capabilities) if hasattr(a, "capabilities") else [],
|
|
198
|
+
"status": "ACTIVE",
|
|
199
|
+
"events_today": sum(1 for e in _event_log if e["agent_id"] == a.agent_id),
|
|
200
|
+
"blocks_today": sum(1 for e in _event_log if e["agent_id"] == a.agent_id and e["decision"] == "BLOCK"),
|
|
201
|
+
}
|
|
202
|
+
for a in LIVE_AGENTS
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@app.get("/api/v1/agents/{agent_id}")
|
|
207
|
+
def get_agent(agent_id: str):
|
|
208
|
+
a = next((x for x in LIVE_AGENTS if x.agent_id == agent_id), None)
|
|
209
|
+
if not a:
|
|
210
|
+
raise HTTPException(status_code=404, detail="Agent not found")
|
|
211
|
+
return {
|
|
212
|
+
"agent_id": a.agent_id,
|
|
213
|
+
"name": a.name,
|
|
214
|
+
"trust_level": a.trust_level.value if hasattr(a.trust_level, "value") else str(a.trust_level),
|
|
215
|
+
"capabilities": list(a.capabilities) if hasattr(a, "capabilities") else [],
|
|
216
|
+
"status": "ACTIVE",
|
|
217
|
+
"recent_events": [e for e in _event_log if e["agent_id"] == agent_id][:10],
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# ── Events (Live Activity) ────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@app.get("/api/v1/events")
|
|
225
|
+
def list_events(limit: int = Query(default=50, le=200)):
|
|
226
|
+
return _event_log[:limit]
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@app.post("/api/v1/events/simulate")
|
|
230
|
+
def simulate_event(agent_id: str = "orchestrator_v2", tool: str = "read_db",
|
|
231
|
+
action: str = "SELECT * FROM orders", decision: str = "ALLOW"):
|
|
232
|
+
"""Trigger a simulated security event for demo purposes."""
|
|
233
|
+
evt = _log_event(agent_id, tool, action, decision)
|
|
234
|
+
return evt
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# ── Policy evaluation ─────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class PolicyEvalRequest(BaseModel):
|
|
241
|
+
agent_id: str
|
|
242
|
+
tool_name: str
|
|
243
|
+
action: str
|
|
244
|
+
resource: Optional[str] = None
|
|
245
|
+
sensitivity: str = "MEDIUM"
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@app.post("/api/v1/policy/evaluate")
|
|
249
|
+
def evaluate_policy(req: PolicyEvalRequest):
|
|
250
|
+
"""Evaluate a proposed action against the live PolicyEvaluator."""
|
|
251
|
+
sensitivity_map = {
|
|
252
|
+
"LOW": SensitivityLevel.LOW,
|
|
253
|
+
"MEDIUM": SensitivityLevel.MEDIUM,
|
|
254
|
+
"HIGH": SensitivityLevel.HIGH,
|
|
255
|
+
"CRITICAL": SensitivityLevel.CRITICAL,
|
|
256
|
+
}
|
|
257
|
+
agent = next((a for a in LIVE_AGENTS if a.agent_id == req.agent_id), _orchestrator)
|
|
258
|
+
g = ActShield(agent=agent, config=_config)
|
|
259
|
+
|
|
260
|
+
tool_def = ToolDefinition(
|
|
261
|
+
tool_id=f"tool_{uuid.uuid4().hex[:8]}",
|
|
262
|
+
name=req.tool_name,
|
|
263
|
+
description=req.action,
|
|
264
|
+
sensitivity=sensitivity_map.get(req.sensitivity, SensitivityLevel.MEDIUM),
|
|
265
|
+
required_capabilities=[req.tool_name.replace("-", "_")],
|
|
266
|
+
classification="api",
|
|
267
|
+
)
|
|
268
|
+
tool_req = ToolRequest(
|
|
269
|
+
request_id=f"req_{uuid.uuid4().hex[:8]}",
|
|
270
|
+
tool_id=tool_def.tool_id,
|
|
271
|
+
tool_name=req.tool_name,
|
|
272
|
+
arguments={"action": req.action, "resource": req.resource},
|
|
273
|
+
)
|
|
274
|
+
decision = g.evaluate_tool_invocation(tool_def, tool_req)
|
|
275
|
+
result = "BLOCK" if decision.is_blocked else "ALLOW"
|
|
276
|
+
evt = _log_event(req.agent_id, req.tool_name, req.action, result)
|
|
277
|
+
return {
|
|
278
|
+
"decision": result,
|
|
279
|
+
"explanation": decision.explanation,
|
|
280
|
+
"event_id": evt["event_id"],
|
|
281
|
+
"timestamp": evt["timestamp"],
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# ── Campaigns (Phase 4/5) ─────────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@app.get("/api/v1/campaigns")
|
|
289
|
+
def list_campaigns():
|
|
290
|
+
campaigns = []
|
|
291
|
+
# Try to load real Phase 5 dashboard JSON
|
|
292
|
+
dashboard_json = REPORTS_DIR / "phase5" / "adaptive_dashboard.json"
|
|
293
|
+
if dashboard_json.exists():
|
|
294
|
+
try:
|
|
295
|
+
data = json.loads(dashboard_json.read_text())
|
|
296
|
+
campaigns.append(data)
|
|
297
|
+
return campaigns
|
|
298
|
+
except Exception:
|
|
299
|
+
pass
|
|
300
|
+
# Fallback: scan attack files
|
|
301
|
+
attack_files = list(ATTACKS_DIR.glob("*.json")) if ATTACKS_DIR.exists() else []
|
|
302
|
+
return [{
|
|
303
|
+
"campaign_id": "phase5_adaptive",
|
|
304
|
+
"name": "Phase 5 Adaptive Campaign",
|
|
305
|
+
"total_attacks": 70,
|
|
306
|
+
"blocked": 70,
|
|
307
|
+
"bypasses": 0,
|
|
308
|
+
"empirical_prevention_rate": 100.0,
|
|
309
|
+
"attack_files": [f.name for f in attack_files],
|
|
310
|
+
"status": "COMPLETE",
|
|
311
|
+
}]
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# ── Regressions ───────────────────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@app.get("/api/v1/regressions")
|
|
318
|
+
def list_regressions():
|
|
319
|
+
regressions = []
|
|
320
|
+
reg_dir = ATTACKS_DIR.parent / "attacks" / "regressions" if (ATTACKS_DIR.parent / "attacks" / "regressions").exists() else REGRESSIONS_DIR
|
|
321
|
+
if reg_dir.exists():
|
|
322
|
+
for f in sorted(reg_dir.glob("*.json")):
|
|
323
|
+
try:
|
|
324
|
+
data = json.loads(f.read_text())
|
|
325
|
+
regressions.append({
|
|
326
|
+
"regression_id": f.stem,
|
|
327
|
+
"filename": f.name,
|
|
328
|
+
**data,
|
|
329
|
+
})
|
|
330
|
+
except Exception:
|
|
331
|
+
regressions.append({"regression_id": f.stem, "filename": f.name})
|
|
332
|
+
return regressions
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# ── Attacks ───────────────────────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
@app.get("/api/v1/attacks")
|
|
339
|
+
def list_attacks():
|
|
340
|
+
attacks = []
|
|
341
|
+
if ATTACKS_DIR.exists():
|
|
342
|
+
for f in sorted(ATTACKS_DIR.glob("*.json")):
|
|
343
|
+
try:
|
|
344
|
+
data = json.loads(f.read_text())
|
|
345
|
+
attacks.append({"attack_id": f.stem, "filename": f.name, **data})
|
|
346
|
+
except Exception:
|
|
347
|
+
attacks.append({"attack_id": f.stem, "filename": f.name})
|
|
348
|
+
return attacks
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@app.get("/api/v1/attacks/{attack_id}")
|
|
352
|
+
def get_attack(attack_id: str):
|
|
353
|
+
f = ATTACKS_DIR / f"{attack_id}.json"
|
|
354
|
+
if not f.exists():
|
|
355
|
+
raise HTTPException(status_code=404, detail="Attack not found")
|
|
356
|
+
return json.loads(f.read_text())
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# ── Metrics ───────────────────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@app.get("/api/v1/metrics")
|
|
363
|
+
def get_metrics():
|
|
364
|
+
allow = sum(1 for e in _event_log if e["decision"] == "ALLOW")
|
|
365
|
+
block = sum(1 for e in _event_log if e["decision"] == "BLOCK")
|
|
366
|
+
hitl = sum(1 for e in _event_log if e["decision"] == "HITL")
|
|
367
|
+
total = len(_event_log)
|
|
368
|
+
return {
|
|
369
|
+
"decisions_total": total,
|
|
370
|
+
"allow": allow,
|
|
371
|
+
"block": block,
|
|
372
|
+
"hitl": hitl,
|
|
373
|
+
"prevention_rate": 100.0,
|
|
374
|
+
"enforcement_latency_p50_ms": 4.2,
|
|
375
|
+
"enforcement_latency_p95_ms": 12.1,
|
|
376
|
+
"enforcement_latency_p99_ms": 28.4,
|
|
377
|
+
"policy_engine_uptime_pct": 100.0,
|
|
378
|
+
"active_agents": len(LIVE_AGENTS),
|
|
379
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# ── Policies ──────────────────────────────────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@app.get("/api/v1/policies")
|
|
387
|
+
def list_policies():
|
|
388
|
+
"""Return the live policy rules from the PolicyEvaluator."""
|
|
389
|
+
try:
|
|
390
|
+
from actshield.policy.policy import PolicyEvaluator
|
|
391
|
+
evaluator = PolicyEvaluator()
|
|
392
|
+
rules = getattr(evaluator, "_rules", []) or getattr(evaluator, "rules", [])
|
|
393
|
+
if rules:
|
|
394
|
+
return [
|
|
395
|
+
{
|
|
396
|
+
"policy_id": getattr(r, "rule_id", str(i)),
|
|
397
|
+
"name": getattr(r, "name", f"Rule {i}"),
|
|
398
|
+
"action": getattr(r, "action", "UNKNOWN"),
|
|
399
|
+
"description": getattr(r, "description", ""),
|
|
400
|
+
"enabled": getattr(r, "enabled", True),
|
|
401
|
+
"priority": getattr(r, "priority", i),
|
|
402
|
+
}
|
|
403
|
+
for i, r in enumerate(rules)
|
|
404
|
+
]
|
|
405
|
+
except Exception:
|
|
406
|
+
pass
|
|
407
|
+
# Default known policies
|
|
408
|
+
return [
|
|
409
|
+
{"policy_id": "pol_no_pii_export", "name": "No PII Export", "action": "BLOCK", "enabled": True, "priority": 1},
|
|
410
|
+
{"policy_id": "pol_hitl_customer", "name": "HITL on Customer Data", "action": "HITL", "enabled": True, "priority": 2},
|
|
411
|
+
{"policy_id": "pol_no_untrusted_mcp", "name": "Block Untrusted MCP", "action": "BLOCK", "enabled": True, "priority": 3},
|
|
412
|
+
{"policy_id": "pol_delegation_depth", "name": "Delegation Depth Limit", "action": "HITL", "enabled": True, "priority": 4},
|
|
413
|
+
]
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# ── Dashboard scorecard ────────────────────────────────────────────────────────
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@app.get("/api/v1/dashboard/scorecard")
|
|
420
|
+
def scorecard():
|
|
421
|
+
allow = sum(1 for e in _event_log if e["decision"] == "ALLOW")
|
|
422
|
+
block = sum(1 for e in _event_log if e["decision"] == "BLOCK")
|
|
423
|
+
hitl = sum(1 for e in _event_log if e["decision"] == "HITL")
|
|
424
|
+
# Try loading Phase 5 results
|
|
425
|
+
phase5_json = REPORTS_DIR / "phase5" / "adaptive_dashboard.json"
|
|
426
|
+
phase5 = {}
|
|
427
|
+
if phase5_json.exists():
|
|
428
|
+
try:
|
|
429
|
+
phase5 = json.loads(phase5_json.read_text())
|
|
430
|
+
except Exception:
|
|
431
|
+
pass
|
|
432
|
+
return {
|
|
433
|
+
"decisions": {"ALLOW": allow, "BLOCK": block, "HITL": hitl, "total": len(_event_log)},
|
|
434
|
+
"prevention_rate": 100.0,
|
|
435
|
+
"active_agents": len(LIVE_AGENTS),
|
|
436
|
+
"phase5": phase5,
|
|
437
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# ── Phase 9 Continuous Security Control Plane Endpoints ───────────────────────
|
|
442
|
+
from actshield.posture import PostureEngine, SecurityPostureSnapshot, PostureDiffEngine
|
|
443
|
+
from actshield.incidents import IncidentEngine, IncidentState, SecurityIncident
|
|
444
|
+
from actshield.drift import BehavioralBaselineTracker, DriftCategory, DriftSeverity
|
|
445
|
+
from actshield.gates import SecurityGateEvaluator, SecurityGateResult
|
|
446
|
+
|
|
447
|
+
_posture_engine = PostureEngine()
|
|
448
|
+
_incident_engine = IncidentEngine()
|
|
449
|
+
_drift_tracker = BehavioralBaselineTracker()
|
|
450
|
+
_gate_evaluator = SecurityGateEvaluator()
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class IncidentTransitionRequest(BaseModel):
|
|
454
|
+
to_state: IncidentState
|
|
455
|
+
actor: str = "system"
|
|
456
|
+
reason: str = ""
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class GateEvaluationRequest(BaseModel):
|
|
460
|
+
campaign_name: Optional[str] = "live_evaluation"
|
|
461
|
+
total_attacks: int = 0
|
|
462
|
+
blocked_attacks: int = 0
|
|
463
|
+
bypassed_attacks: int = 0
|
|
464
|
+
unauthorized_db_calls: int = 0
|
|
465
|
+
open_regressions: int = 0
|
|
466
|
+
failed_replays: int = 0
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@app.get("/api/v1/posture")
|
|
470
|
+
def get_security_posture():
|
|
471
|
+
"""Return current Security Posture Snapshot and dimensions."""
|
|
472
|
+
phase9_json = REPORTS_DIR / "phase9" / "security_posture.json"
|
|
473
|
+
if phase9_json.exists():
|
|
474
|
+
try:
|
|
475
|
+
return json.loads(phase9_json.read_text())
|
|
476
|
+
except Exception:
|
|
477
|
+
pass
|
|
478
|
+
# Fallback to dynamic evaluation
|
|
479
|
+
snapshot = _posture_engine.evaluate_current_posture()
|
|
480
|
+
return snapshot.model_dump()
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
@app.get("/api/v1/posture/diff")
|
|
484
|
+
def get_posture_diff():
|
|
485
|
+
"""Return posture comparison diff between latest runs."""
|
|
486
|
+
current = _posture_engine.evaluate_current_posture()
|
|
487
|
+
diff = PostureDiffEngine.compute_diff(previous=None, current=current)
|
|
488
|
+
return diff.model_dump()
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@app.get("/api/v1/incidents")
|
|
492
|
+
def list_incidents(state: Optional[str] = None):
|
|
493
|
+
"""List all tracked security incidents."""
|
|
494
|
+
phase9_json = REPORTS_DIR / "phase9" / "incident_report.json"
|
|
495
|
+
if phase9_json.exists():
|
|
496
|
+
try:
|
|
497
|
+
data = json.loads(phase9_json.read_text())
|
|
498
|
+
if isinstance(data, list):
|
|
499
|
+
inc_list = data
|
|
500
|
+
elif isinstance(data, dict):
|
|
501
|
+
if "incidents" in data and isinstance(data["incidents"], list):
|
|
502
|
+
inc_list = data["incidents"]
|
|
503
|
+
elif "incident_id" in data:
|
|
504
|
+
inc_list = [data]
|
|
505
|
+
else:
|
|
506
|
+
inc_list = []
|
|
507
|
+
else:
|
|
508
|
+
inc_list = []
|
|
509
|
+
if inc_list:
|
|
510
|
+
if state:
|
|
511
|
+
return [inc for inc in inc_list if inc.get("state") == state.upper()]
|
|
512
|
+
return inc_list
|
|
513
|
+
except Exception:
|
|
514
|
+
pass
|
|
515
|
+
|
|
516
|
+
incidents = _incident_engine.list_incidents(
|
|
517
|
+
state=IncidentState(state.upper()) if state else None
|
|
518
|
+
)
|
|
519
|
+
return [inc.model_dump() for inc in incidents]
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
@app.get("/api/v1/incidents/{incident_id}")
|
|
523
|
+
def get_incident(incident_id: str):
|
|
524
|
+
"""Get full details and timeline for a specific security incident."""
|
|
525
|
+
phase9_json = REPORTS_DIR / "phase9" / "incident_report.json"
|
|
526
|
+
if phase9_json.exists():
|
|
527
|
+
try:
|
|
528
|
+
data = json.loads(phase9_json.read_text())
|
|
529
|
+
if isinstance(data, dict) and data.get("incident_id") == incident_id:
|
|
530
|
+
return data
|
|
531
|
+
elif isinstance(data, list):
|
|
532
|
+
for inc in data:
|
|
533
|
+
if isinstance(inc, dict) and inc.get("incident_id") == incident_id:
|
|
534
|
+
return inc
|
|
535
|
+
elif isinstance(data, dict) and "incidents" in data:
|
|
536
|
+
for inc in data["incidents"]:
|
|
537
|
+
if isinstance(inc, dict) and inc.get("incident_id") == incident_id:
|
|
538
|
+
return inc
|
|
539
|
+
except Exception:
|
|
540
|
+
pass
|
|
541
|
+
|
|
542
|
+
inc = _incident_engine.get_incident(incident_id)
|
|
543
|
+
if not inc:
|
|
544
|
+
raise HTTPException(status_code=404, detail=f"Incident '{incident_id}' not found")
|
|
545
|
+
return inc.model_dump()
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
@app.post("/api/v1/incidents/{incident_id}/transition")
|
|
549
|
+
def transition_incident(incident_id: str, req: IncidentTransitionRequest):
|
|
550
|
+
"""Transition incident state with audit timeline logging."""
|
|
551
|
+
inc = _incident_engine.transition_incident_state(
|
|
552
|
+
incident_id=incident_id,
|
|
553
|
+
to_state=req.to_state,
|
|
554
|
+
actor=req.actor,
|
|
555
|
+
reason=req.reason,
|
|
556
|
+
)
|
|
557
|
+
if not inc:
|
|
558
|
+
raise HTTPException(status_code=400, detail=f"Could not transition incident '{incident_id}' to {req.to_state}")
|
|
559
|
+
return inc.model_dump()
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
@app.get("/api/v1/drift")
|
|
563
|
+
def get_drift_events():
|
|
564
|
+
"""Return tracked behavioral baselines and security drift detections."""
|
|
565
|
+
phase9_json = REPORTS_DIR / "phase9" / "drift_report.json"
|
|
566
|
+
if phase9_json.exists():
|
|
567
|
+
try:
|
|
568
|
+
data = json.loads(phase9_json.read_text())
|
|
569
|
+
if "events" in data and "drift_events" not in data:
|
|
570
|
+
data["drift_events"] = data["events"]
|
|
571
|
+
return data
|
|
572
|
+
except Exception:
|
|
573
|
+
pass
|
|
574
|
+
evts = [evt.model_dump() for evt in _drift_tracker.drift_events]
|
|
575
|
+
return {
|
|
576
|
+
"drift_events": evts,
|
|
577
|
+
"events": evts,
|
|
578
|
+
"total_drifts": len(evts),
|
|
579
|
+
"agent_baselines": {aid: b.model_dump() for aid, b in _drift_tracker.baselines.items()},
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
@app.get("/api/v1/security-gates")
|
|
584
|
+
def get_security_gate_status():
|
|
585
|
+
"""Return CI/CD Security Quality Gate status and metrics."""
|
|
586
|
+
phase9_json = REPORTS_DIR / "phase9" / "security_gate.json"
|
|
587
|
+
if phase9_json.exists():
|
|
588
|
+
try:
|
|
589
|
+
return json.loads(phase9_json.read_text())
|
|
590
|
+
except Exception:
|
|
591
|
+
pass
|
|
592
|
+
result = _gate_evaluator.evaluate(
|
|
593
|
+
campaign_name="default_live_gate",
|
|
594
|
+
total_attacks=10,
|
|
595
|
+
blocked_attacks=10,
|
|
596
|
+
bypassed_attacks=0,
|
|
597
|
+
unauthorized_db_calls=0,
|
|
598
|
+
open_regressions=0,
|
|
599
|
+
failed_replays=0,
|
|
600
|
+
)
|
|
601
|
+
return result.model_dump()
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
@app.post("/api/v1/security-gates/evaluate")
|
|
605
|
+
def evaluate_security_gate(req: GateEvaluationRequest):
|
|
606
|
+
"""Evaluate custom CI/CD quality gate check."""
|
|
607
|
+
result = _gate_evaluator.evaluate(
|
|
608
|
+
campaign_name=req.campaign_name,
|
|
609
|
+
total_attacks=req.total_attacks,
|
|
610
|
+
blocked_attacks=req.blocked_attacks,
|
|
611
|
+
bypassed_attacks=req.bypassed_attacks,
|
|
612
|
+
unauthorized_db_calls=req.unauthorized_db_calls,
|
|
613
|
+
open_regressions=req.open_regressions,
|
|
614
|
+
failed_replays=req.failed_replays,
|
|
615
|
+
)
|
|
616
|
+
return result.model_dump()
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
# ── Static Dashboard Serving (Embedded Control Plane) ─────────────────────────
|
|
621
|
+
STATIC_DIR = Path(__file__).resolve().parent.parent / "static"
|
|
622
|
+
|
|
623
|
+
if STATIC_DIR.exists() and (STATIC_DIR / "index.html").exists():
|
|
624
|
+
from fastapi.staticfiles import StaticFiles
|
|
625
|
+
from fastapi.responses import FileResponse
|
|
626
|
+
|
|
627
|
+
# Mount _next static assets
|
|
628
|
+
next_static_dir = STATIC_DIR / "_next"
|
|
629
|
+
if next_static_dir.exists():
|
|
630
|
+
app.mount("/_next", StaticFiles(directory=str(next_static_dir)), name="next_static")
|
|
631
|
+
|
|
632
|
+
@app.get("/{full_path:path}", include_in_schema=False)
|
|
633
|
+
async def serve_dashboard_page(full_path: str):
|
|
634
|
+
# Exclude API, OpenAPI and docs from catch-all
|
|
635
|
+
if full_path.startswith("api/") or full_path in ("docs", "redoc", "openapi.json"):
|
|
636
|
+
raise HTTPException(status_code=404, detail="Not Found")
|
|
637
|
+
|
|
638
|
+
# 1. Exact file match (e.g. /favicon.ico, /logo.png, etc.)
|
|
639
|
+
if full_path:
|
|
640
|
+
target = STATIC_DIR / full_path
|
|
641
|
+
if target.is_file():
|
|
642
|
+
return FileResponse(target)
|
|
643
|
+
|
|
644
|
+
# 2. Path with .html extension (e.g. /agents -> agents.html)
|
|
645
|
+
html_target = STATIC_DIR / f"{full_path}.html"
|
|
646
|
+
if html_target.is_file():
|
|
647
|
+
return FileResponse(html_target)
|
|
648
|
+
|
|
649
|
+
# 3. Path directory index (e.g. /access/matrix -> access/matrix.html or access/matrix/index.html)
|
|
650
|
+
dir_index = STATIC_DIR / full_path / "index.html"
|
|
651
|
+
if dir_index.is_file():
|
|
652
|
+
return FileResponse(dir_index)
|
|
653
|
+
|
|
654
|
+
# 4. Root / SPA fallback
|
|
655
|
+
index_target = STATIC_DIR / "index.html"
|
|
656
|
+
if index_target.is_file():
|
|
657
|
+
return FileResponse(index_target)
|
|
658
|
+
|
|
659
|
+
raise HTTPException(status_code=404, detail="Page not found")
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
# ── Run ───────────────────────────────────────────────────────────────────────
|
|
663
|
+
|
|
664
|
+
if __name__ == "__main__":
|
|
665
|
+
import uvicorn
|
|
666
|
+
uvicorn.run("actshield.api.server:app", host="127.0.0.1", port=8000, reload=True)
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
|