admina-framework 0.12.0__tar.gz → 0.12.2__tar.gz
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.
- {admina_framework-0.12.0/admina_framework.egg-info → admina_framework-0.12.2}/PKG-INFO +5 -1
- {admina_framework-0.12.0 → admina_framework-0.12.2}/README.md +4 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/__init__.py +1 -1
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/main.py +1 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/dashboard/static/index.html +156 -7
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/compliance/forensic.py +21 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/engines/presidio.py +6 -1
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/auth/apikey.py +10 -47
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/api/dashboard.py +213 -41
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/api/gateway.py +165 -4
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/config.py +16 -4
- admina_framework-0.12.2/admina/proxy/dashboard_session.py +133 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/main.py +191 -81
- {admina_framework-0.12.0 → admina_framework-0.12.2/admina_framework.egg-info}/PKG-INFO +5 -1
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina_framework.egg-info/SOURCES.txt +2 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/pyproject.toml +1 -1
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_forensic_blackbox.py +41 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_gateway_endpoints.py +331 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_gateway_mount.py +71 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_presidio_engine.py +27 -0
- admina_framework-0.12.2/tests/test_proxy_dashboard_session.py +800 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_proxy_security.py +25 -72
- {admina_framework-0.12.0 → admina_framework-0.12.2}/LICENSE +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/NOTICE +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/commands/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/templates/admina.yaml.j2 +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/templates/docker-compose.yml.j2 +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/templates/env.j2 +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/templates/main.py.j2 +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/templates/plugin.py.j2 +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/templates/plugin_pyproject.toml.j2 +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/templates/plugin_readme.md.j2 +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/templates/plugin_test.py.j2 +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/core/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/core/config.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/core/event_bus.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/core/secrets.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/core/types.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/dashboard/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/dashboard/static/heimdall.png +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/dashboard/static/vendor/alpinejs.min.js +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/agent_security/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/agent_security/coordination.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/agent_security/egress.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/agent_security/fingerprint.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/agent_security/firewall.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/agent_security/loop_breaker.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/ai_infra/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/ai_infra/llm_engine.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/ai_infra/rag.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/ai_infra/webui.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/compliance/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/compliance/cross_regulation.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/compliance/eu_ai_act.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/compliance/gdpr.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/compliance/nis2.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/compliance/oisg.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/compliance/otel.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/data_sovereignty/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/data_sovereignty/classification.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/data_sovereignty/pii.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/data_sovereignty/residency.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/governance.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/engines/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/integrations/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/integrations/_engines.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/integrations/cheshirecat/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/integrations/cheshirecat/admina-plugin/admina_governance.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/integrations/crewai/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/integrations/crewai/callbacks.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/integrations/langchain/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/integrations/langchain/callbacks.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/integrations/n8n/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/integrations/openclaw/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/base.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/adapters/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/adapters/_streaming.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/adapters/anthropic.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/adapters/bedrock.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/adapters/gemini.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/adapters/mistral.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/adapters/ollama.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/adapters/openai.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/adapters/vllm.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/alerts/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/alerts/log.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/alerts/webhook.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/auth/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/compliance/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/compliance/eu_ai_act.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/connectors/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/connectors/chromadb.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/connectors/filesystem.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/forensic/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/forensic/filesystem.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/guards/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/guards/guardrailsai_guard.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/pii/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/pii/spacy_regex.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/transports/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/transports/http_rest.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/transports/mcp.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/registry.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/api/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/api/integration.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/engine_bridge.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/governance.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/multi_upstream.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/state.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/py.typed +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/baselines/baseline.json +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/corpora/SHA256SUMS +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/corpora/coordination.jsonl +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/corpora/injection.jsonl +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/corpora/loop.jsonl +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/corpora/pii.jsonl +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/corpora.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/detectors.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/gate.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/metrics.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/report.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/runner.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/sdk/__init__.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/sdk/_compat.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/sdk/compliance_kit.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/sdk/errors.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/sdk/governed_agent.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/sdk/governed_data.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/sdk/governed_model.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/sdk/retry.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/sdk/streaming.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina_framework.egg-info/dependency_links.txt +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina_framework.egg-info/entry_points.txt +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina_framework.egg-info/requires.txt +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/admina_framework.egg-info/top_level.txt +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/setup.cfg +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_benchmark_14us.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_coordination.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_coordination_cli.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_coordination_corpus.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_coordination_wiring.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_core_config.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_domains.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_domains_governance.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_egress_analyze.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_egress_cli.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_egress_pipeline.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_egress_policy.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_egress_surfaces.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_engines.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_error_disclosure.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_fingerprint.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_firewall_parity.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_gateway_config.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_gateway_guard_fail_mode.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_gateway_helpers.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_governance_pipeline.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_guard_fail_mode.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_mcp_transport.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_proxy_forensic_would_action.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_proxy_governance_mode_config.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_proxy_guard_fail_mode.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_proxy_mcp_response_pii.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_proxy_metrics_endpoint.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_proxy_plugins.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_redteam_efficacy.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_redteam_lib.py +0 -0
- {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: admina-framework
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.2
|
|
4
4
|
Summary: Admina — governed AI development framework
|
|
5
5
|
Author-email: Stefano Noferi <info@admina.org>
|
|
6
6
|
Maintainer-email: Stefano Noferi <info@admina.org>
|
|
@@ -382,6 +382,10 @@ Real-time governance dashboard on port 3000:
|
|
|
382
382
|
|
|
383
383
|
API backend: `GET /api/dashboard/score`, `/feed`, `/compliance`, `/sovereignty`, `/infra`, `/models`
|
|
384
384
|
|
|
385
|
+
In `admina dev` local mode the dashboard asks for the API key (`admina password show`)
|
|
386
|
+
and keeps a short-lived browser session that only the read-only dashboard API accepts.
|
|
387
|
+
Set `ADMINA_DASHBOARD_ENABLED=false` to stop serving the bundled dashboard.
|
|
388
|
+
|
|
385
389
|
## Configuration
|
|
386
390
|
|
|
387
391
|
Admina uses `admina.yaml` as the primary config file (with `.env` fallback for backward compatibility):
|
|
@@ -305,6 +305,10 @@ Real-time governance dashboard on port 3000:
|
|
|
305
305
|
|
|
306
306
|
API backend: `GET /api/dashboard/score`, `/feed`, `/compliance`, `/sovereignty`, `/infra`, `/models`
|
|
307
307
|
|
|
308
|
+
In `admina dev` local mode the dashboard asks for the API key (`admina password show`)
|
|
309
|
+
and keeps a short-lived browser session that only the read-only dashboard API accepts.
|
|
310
|
+
Set `ADMINA_DASHBOARD_ENABLED=false` to stop serving the bundled dashboard.
|
|
311
|
+
|
|
308
312
|
## Configuration
|
|
309
313
|
|
|
310
314
|
Admina uses `admina.yaml` as the primary config file (with `.env` fallback for backward compatibility):
|
|
@@ -643,6 +643,7 @@ def _run_local(
|
|
|
643
643
|
click.echo(f" http://{ip}:{port} ({label})")
|
|
644
644
|
else:
|
|
645
645
|
click.echo(f" Ready → http://{display_host}:{port}")
|
|
646
|
+
click.echo(" Dashboard sign-in: the API key (show it with `admina password show`)")
|
|
646
647
|
if not no_browser:
|
|
647
648
|
webbrowser.open(f"http://{display_host}:{port}")
|
|
648
649
|
else:
|
|
@@ -330,6 +330,33 @@
|
|
|
330
330
|
.link-card:hover { border-color: var(--teal); }
|
|
331
331
|
.link-card .lc-label { font-size: 11px; text-transform: uppercase; color: var(--muted); margin-bottom: 6px; }
|
|
332
332
|
.link-card .lc-title { font-size: 14px; font-weight: 600; }
|
|
333
|
+
|
|
334
|
+
/* ── Sign-in ─────────────────────────────────────────── */
|
|
335
|
+
.signin {
|
|
336
|
+
position: fixed; inset: 0; z-index: 300; background: var(--bg);
|
|
337
|
+
display: flex; align-items: center; justify-content: center; padding: 24px;
|
|
338
|
+
}
|
|
339
|
+
.signin-card {
|
|
340
|
+
width: 100%; max-width: 420px; background: var(--surface);
|
|
341
|
+
border: 1px solid var(--border2); border-radius: 12px; padding: 28px;
|
|
342
|
+
}
|
|
343
|
+
.signin-card .logo { display: flex; align-items: center; gap: 10px; font-size: 18px; font-weight: 700; margin-bottom: 16px; }
|
|
344
|
+
.signin-card .logo img { width: 32px; height: 32px; object-fit: contain; }
|
|
345
|
+
.signin-card .logo .accent { color: var(--teal); }
|
|
346
|
+
.signin-card p { font-size: 13px; color: var(--muted); line-height: 1.6; margin-bottom: 16px; }
|
|
347
|
+
.signin-card code { font-family: var(--mono); font-size: 12px; color: var(--text); }
|
|
348
|
+
.signin-card label { display: block; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin-bottom: 6px; }
|
|
349
|
+
.signin-card input {
|
|
350
|
+
width: 100%; padding: 9px 12px; border-radius: 6px; border: 1px solid var(--border2);
|
|
351
|
+
background: var(--surface2); color: var(--text); font-family: var(--mono); font-size: 13px;
|
|
352
|
+
}
|
|
353
|
+
.signin-card input:focus { outline: none; border-color: var(--teal); }
|
|
354
|
+
.signin-card button {
|
|
355
|
+
margin-top: 14px; width: 100%; padding: 9px 12px; border-radius: 6px; cursor: pointer;
|
|
356
|
+
background: var(--teal-dark); color: #fff; border: 1px solid var(--teal); font-size: 14px; font-weight: 600;
|
|
357
|
+
}
|
|
358
|
+
.signin-card button:disabled { opacity: 0.6; cursor: default; }
|
|
359
|
+
.signin-card .error { margin-top: 12px; font-size: 13px; color: var(--red); }
|
|
333
360
|
</style>
|
|
334
361
|
</head>
|
|
335
362
|
<body>
|
|
@@ -339,6 +366,25 @@
|
|
|
339
366
|
════════════════════════════════════════════════════════ -->
|
|
340
367
|
<div x-data="dashboard()" x-init="init()">
|
|
341
368
|
|
|
369
|
+
<!-- Sign-in: the API key is exchanged for a short-lived browser session -->
|
|
370
|
+
<div class="signin" x-show="needsLogin" x-cloak>
|
|
371
|
+
<form class="signin-card" @submit.prevent="login()">
|
|
372
|
+
<div class="logo">
|
|
373
|
+
<img src="heimdall.png" alt="">
|
|
374
|
+
<span><span class="accent">Admina</span> Governance Dashboard</span>
|
|
375
|
+
</div>
|
|
376
|
+
<p>
|
|
377
|
+
Sign in with this instance's API key. In an <code>admina dev</code> project,
|
|
378
|
+
run <code>admina password show</code> to display it.
|
|
379
|
+
</p>
|
|
380
|
+
<label for="admina-api-key">API key</label>
|
|
381
|
+
<input id="admina-api-key" type="password" x-model="loginKey" x-ref="loginKey"
|
|
382
|
+
autocomplete="off" autocapitalize="off" spellcheck="false" required>
|
|
383
|
+
<button type="submit" :disabled="loginBusy || !loginKey">Sign in</button>
|
|
384
|
+
<div class="error" x-show="loginError" x-text="loginError" role="alert"></div>
|
|
385
|
+
</form>
|
|
386
|
+
</div>
|
|
387
|
+
|
|
342
388
|
<!-- Top Bar -->
|
|
343
389
|
<div class="topbar">
|
|
344
390
|
<div class="logo">
|
|
@@ -349,6 +395,9 @@
|
|
|
349
395
|
<span class="ts" x-text="lastUpdate"></span>
|
|
350
396
|
<div class="sep"></div>
|
|
351
397
|
<button class="refresh-btn" @click="refresh()">Refresh</button>
|
|
398
|
+
<template x-if="sessionActive">
|
|
399
|
+
<button class="refresh-btn" @click="logout()">Sign out</button>
|
|
400
|
+
</template>
|
|
352
401
|
<div class="sep"></div>
|
|
353
402
|
<div class="status">
|
|
354
403
|
<div class="dot" :style="{ background: healthy ? 'var(--green)' : 'var(--red)' }"></div>
|
|
@@ -686,12 +735,14 @@
|
|
|
686
735
|
</p>
|
|
687
736
|
<p>
|
|
688
737
|
Until you submit a real assessment, the dashboard renders all articles at 0% with a default
|
|
689
|
-
placeholder. To record your real coverage,
|
|
690
|
-
<code class="inline">/api/compliance/gap-analysis</code
|
|
738
|
+
placeholder. To record your real coverage, send your declared evidence with a
|
|
739
|
+
<strong>POST</strong> request to <code class="inline">/api/compliance/gap-analysis</code>
|
|
740
|
+
on this host (the route accepts POST only: opening it in the browser answers
|
|
741
|
+
405 Method Not Allowed).
|
|
691
742
|
</p>
|
|
692
743
|
<p style="margin-bottom: 6px;"><strong>Example request:</strong></p>
|
|
693
744
|
<button class="modal-copy-btn" @click="copyAssessmentCmd($event)">Copy command</button>
|
|
694
|
-
<pre class="modal-pre" id="assessment-cmd">curl -X POST
|
|
745
|
+
<pre class="modal-pre" id="assessment-cmd">curl -X POST <span x-text="apiBase"></span>/api/compliance/gap-analysis \
|
|
695
746
|
-H "X-API-Key: $ADMINA_API_KEY" -H "Content-Type: application/json" \
|
|
696
747
|
-d '{"risk_category": "high",
|
|
697
748
|
"current_compliance": {
|
|
@@ -807,7 +858,9 @@
|
|
|
807
858
|
* Admina Governance Dashboard — Alpine.js application.
|
|
808
859
|
*
|
|
809
860
|
* Reads from the Phase 3.1 API endpoints. Does NOT write.
|
|
810
|
-
* All API calls go
|
|
861
|
+
* All API calls go to /api/* on the same origin: the proxy itself in
|
|
862
|
+
* `admina dev` local mode (browser session after sign-in), or the nginx
|
|
863
|
+
* reverse proxy of the Docker stack (which adds the API key).
|
|
811
864
|
*/
|
|
812
865
|
function dashboard() {
|
|
813
866
|
return {
|
|
@@ -824,6 +877,12 @@ function dashboard() {
|
|
|
824
877
|
modelData: {},
|
|
825
878
|
oisg: {},
|
|
826
879
|
showAssessmentHelp: false,
|
|
880
|
+
needsLogin: false,
|
|
881
|
+
sessionActive: false,
|
|
882
|
+
loginKey: '',
|
|
883
|
+
loginError: '',
|
|
884
|
+
loginBusy: false,
|
|
885
|
+
_started: false,
|
|
827
886
|
_ws: null,
|
|
828
887
|
_feedId: 0,
|
|
829
888
|
_refreshTimer: null,
|
|
@@ -920,17 +979,96 @@ function dashboard() {
|
|
|
920
979
|
|
|
921
980
|
// ── Init ───────────────────────────────────────────
|
|
922
981
|
async init() {
|
|
923
|
-
await this.
|
|
982
|
+
if (await this.checkSession()) this.start();
|
|
983
|
+
},
|
|
984
|
+
|
|
985
|
+
start() {
|
|
986
|
+
// Alpine calls init() both automatically and via x-init: start once.
|
|
987
|
+
if (this._started) return;
|
|
988
|
+
this._started = true;
|
|
989
|
+
this.refresh();
|
|
924
990
|
this.connectWebSocket();
|
|
925
991
|
// Auto-refresh every 10s
|
|
926
992
|
this._refreshTimer = setInterval(() => this.refresh(), 10000);
|
|
927
993
|
},
|
|
928
994
|
|
|
995
|
+
// ── Session ────────────────────────────────────────
|
|
996
|
+
// Serving this page never signs the browser in. The API key is exchanged
|
|
997
|
+
// once for a short-lived HttpOnly session cookie that only the read-only
|
|
998
|
+
// dashboard API accepts. Behind the Docker nginx the proxy already
|
|
999
|
+
// receives the key, so no sign-in is needed there.
|
|
1000
|
+
async checkSession() {
|
|
1001
|
+
try {
|
|
1002
|
+
const r = await fetch('/api/dashboard/session', { cache: 'no-store' });
|
|
1003
|
+
if (r.status === 401) { this.showLogin(''); return false; }
|
|
1004
|
+
if (r.ok) this.sessionActive = !!(await r.json()).session;
|
|
1005
|
+
} catch (e) { /* network error: start anyway and show Disconnected */ }
|
|
1006
|
+
return true;
|
|
1007
|
+
},
|
|
1008
|
+
|
|
1009
|
+
async login() {
|
|
1010
|
+
if (!this.loginKey) return;
|
|
1011
|
+
this.loginBusy = true;
|
|
1012
|
+
this.loginError = '';
|
|
1013
|
+
try {
|
|
1014
|
+
const r = await fetch('/api/dashboard/session', {
|
|
1015
|
+
method: 'POST',
|
|
1016
|
+
headers: { 'X-API-Key': this.loginKey },
|
|
1017
|
+
cache: 'no-store',
|
|
1018
|
+
});
|
|
1019
|
+
if (r.ok) {
|
|
1020
|
+
this.loginKey = '';
|
|
1021
|
+
this.needsLogin = false;
|
|
1022
|
+
this.sessionActive = true;
|
|
1023
|
+
this.start();
|
|
1024
|
+
} else if (r.status === 401) {
|
|
1025
|
+
this.loginError = 'Invalid API key.';
|
|
1026
|
+
} else {
|
|
1027
|
+
let detail = '';
|
|
1028
|
+
try { detail = (await r.json()).detail || ''; } catch (e) { /* not JSON */ }
|
|
1029
|
+
this.loginError = detail || `Sign-in failed (HTTP ${r.status}).`;
|
|
1030
|
+
}
|
|
1031
|
+
} catch (e) {
|
|
1032
|
+
this.loginError = 'Admina is not reachable.';
|
|
1033
|
+
} finally {
|
|
1034
|
+
this.loginBusy = false;
|
|
1035
|
+
}
|
|
1036
|
+
},
|
|
1037
|
+
|
|
1038
|
+
async logout() {
|
|
1039
|
+
try { await fetch('/api/dashboard/session', { method: 'DELETE', cache: 'no-store' }); }
|
|
1040
|
+
catch (e) { /* cookie expires on its own */ }
|
|
1041
|
+
this.showLogin('');
|
|
1042
|
+
},
|
|
1043
|
+
|
|
1044
|
+
showLogin(message) {
|
|
1045
|
+
clearInterval(this._refreshTimer);
|
|
1046
|
+
this._refreshTimer = null;
|
|
1047
|
+
this._started = false;
|
|
1048
|
+
if (this._ws) {
|
|
1049
|
+
const ws = this._ws;
|
|
1050
|
+
this._ws = null;
|
|
1051
|
+
ws.onclose = null;
|
|
1052
|
+
ws.close();
|
|
1053
|
+
}
|
|
1054
|
+
this.wsConnected = false;
|
|
1055
|
+
this.sessionActive = false;
|
|
1056
|
+
this.feedEvents = [];
|
|
1057
|
+
this.loginError = message;
|
|
1058
|
+
this.needsLogin = true;
|
|
1059
|
+
this.$nextTick(() => this.$refs.loginKey && this.$refs.loginKey.focus());
|
|
1060
|
+
},
|
|
1061
|
+
|
|
929
1062
|
// ── Fetch all data ─────────────────────────────────
|
|
930
1063
|
async refresh() {
|
|
931
1064
|
// allSettled so one failing endpoint never blanks the whole dashboard.
|
|
932
1065
|
const okJson = async (url) => {
|
|
933
1066
|
const r = await fetch(url);
|
|
1067
|
+
if (r.status === 401) {
|
|
1068
|
+
const err = new Error(`${url} -> 401`);
|
|
1069
|
+
err.unauthorized = true;
|
|
1070
|
+
throw err;
|
|
1071
|
+
}
|
|
934
1072
|
if (!r.ok) throw new Error(`${url} -> ${r.status}`);
|
|
935
1073
|
return r.json();
|
|
936
1074
|
};
|
|
@@ -944,6 +1082,10 @@ function dashboard() {
|
|
|
944
1082
|
okJson('/api/dashboard/models'),
|
|
945
1083
|
okJson('/api/dashboard/oisg'),
|
|
946
1084
|
]);
|
|
1085
|
+
if (results.some(r => r.status === 'rejected' && r.reason && r.reason.unauthorized)) {
|
|
1086
|
+
this.showLogin('Your session has expired. Sign in again.');
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
947
1089
|
const [scoreRes, compRes, sovRes, statsRes, feedRes, infraRes, modelsRes, oisgRes] =
|
|
948
1090
|
results.map(r => r.status === 'fulfilled' ? r.value : null);
|
|
949
1091
|
|
|
@@ -998,8 +1140,9 @@ function dashboard() {
|
|
|
998
1140
|
|
|
999
1141
|
this._ws.onclose = () => {
|
|
1000
1142
|
this.wsConnected = false;
|
|
1143
|
+
if (this.needsLogin) return;
|
|
1001
1144
|
console.log('[Admina] WebSocket disconnected, reconnecting in 3s...');
|
|
1002
|
-
setTimeout(() => this.connectWebSocket(), 3000);
|
|
1145
|
+
setTimeout(() => { if (!this.needsLogin) this.connectWebSocket(); }, 3000);
|
|
1003
1146
|
};
|
|
1004
1147
|
|
|
1005
1148
|
this._ws.onerror = () => {
|
|
@@ -1007,11 +1150,17 @@ function dashboard() {
|
|
|
1007
1150
|
};
|
|
1008
1151
|
} catch (e) {
|
|
1009
1152
|
this.wsConnected = false;
|
|
1010
|
-
setTimeout(() => this.connectWebSocket(), 5000);
|
|
1153
|
+
setTimeout(() => { if (!this.needsLogin) this.connectWebSocket(); }, 5000);
|
|
1011
1154
|
}
|
|
1012
1155
|
},
|
|
1013
1156
|
|
|
1014
1157
|
// ── Helpers ─────────────────────────────────────────
|
|
1158
|
+
// The origin that served this page: the proxy itself, or the dashboard
|
|
1159
|
+
// container that forwards /api/ to it.
|
|
1160
|
+
get apiBase() {
|
|
1161
|
+
return window.location.origin;
|
|
1162
|
+
},
|
|
1163
|
+
|
|
1015
1164
|
copyAssessmentCmd(ev) {
|
|
1016
1165
|
const pre = document.getElementById('assessment-cmd');
|
|
1017
1166
|
if (!pre) return;
|
|
@@ -24,6 +24,7 @@ import logging
|
|
|
24
24
|
import os
|
|
25
25
|
import threading
|
|
26
26
|
import time
|
|
27
|
+
from collections import deque
|
|
27
28
|
from datetime import UTC, datetime
|
|
28
29
|
from pathlib import Path
|
|
29
30
|
|
|
@@ -35,6 +36,8 @@ logger = logging.getLogger("admina.forensic_blackbox")
|
|
|
35
36
|
_CHAIN_STATE_KEY = "_chain_state.json"
|
|
36
37
|
# Sidecar holding the HMAC-SHA256 signature of the chain-state payload.
|
|
37
38
|
_CHAIN_STATE_SIG_KEY = "_chain_state.json.sig"
|
|
39
|
+
# Records kept in memory for recent_records(), newest last.
|
|
40
|
+
RECENT_RECORDS_MAX = 1000
|
|
38
41
|
|
|
39
42
|
|
|
40
43
|
class ForensicBlackBox(BaseForensicStore):
|
|
@@ -81,6 +84,10 @@ class ForensicBlackBox(BaseForensicStore):
|
|
|
81
84
|
self.chain_head: str = "GENESIS"
|
|
82
85
|
self.record_count: int = 0
|
|
83
86
|
self._write_lock = threading.Lock()
|
|
87
|
+
# The last records written by this process, for readers that need
|
|
88
|
+
# recent activity without reading the backend back (the dashboard
|
|
89
|
+
# feed when no analytics store is configured).
|
|
90
|
+
self._recent: deque[dict] = deque(maxlen=RECENT_RECORDS_MAX)
|
|
84
91
|
self._state_signing_key = state_signing_key or os.environ.get("ADMINA_FORENSIC_STATE_KEY")
|
|
85
92
|
if self.filesystem_dir is not None:
|
|
86
93
|
self.filesystem_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -346,6 +353,7 @@ class ForensicBlackBox(BaseForensicStore):
|
|
|
346
353
|
# Store the record and persist the updated chain state
|
|
347
354
|
self._store_to_s3(forensic_record)
|
|
348
355
|
self._persist_chain_state()
|
|
356
|
+
self._recent.append(forensic_record)
|
|
349
357
|
|
|
350
358
|
return {
|
|
351
359
|
"sequence_number": self.record_count,
|
|
@@ -354,6 +362,19 @@ class ForensicBlackBox(BaseForensicStore):
|
|
|
354
362
|
"stored": (self.boto3_client is not None or self.filesystem_dir is not None),
|
|
355
363
|
}
|
|
356
364
|
|
|
365
|
+
def recent_records(self, limit: int = RECENT_RECORDS_MAX) -> list[dict]:
|
|
366
|
+
"""Return up to *limit* records written by this process, newest first.
|
|
367
|
+
|
|
368
|
+
Only the last ``RECENT_RECORDS_MAX`` records are kept, in memory, with
|
|
369
|
+
every backend; records written before the process started are not
|
|
370
|
+
read back. Use :meth:`verify_chain` for the persisted chain.
|
|
371
|
+
"""
|
|
372
|
+
if limit <= 0:
|
|
373
|
+
return []
|
|
374
|
+
with self._write_lock:
|
|
375
|
+
records = list(self._recent)
|
|
376
|
+
return records[::-1][:limit]
|
|
377
|
+
|
|
357
378
|
def _store_to_s3(self, record: dict):
|
|
358
379
|
"""Persist a forensic record using the configured backend."""
|
|
359
380
|
ts = datetime.now(UTC)
|
|
@@ -57,6 +57,9 @@ _PRESIDIO_TO_ADMINA: dict[str, str] = {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
# Languages Admina configures for Presidio, each mapped to its spaCy model.
|
|
60
|
+
# The Presidio entity types requested from the analyzer.
|
|
61
|
+
_ENTITIES: list[str] = list(_PRESIDIO_TO_ADMINA)
|
|
62
|
+
|
|
60
63
|
_LANG_MODELS: dict[str, str] = {
|
|
61
64
|
"en": "en_core_web_sm",
|
|
62
65
|
"it": "it_core_news_sm",
|
|
@@ -111,9 +114,11 @@ class PresidioPIIEngine:
|
|
|
111
114
|
|
|
112
115
|
# Union of per-language analyses: a recognizer registered for only one
|
|
113
116
|
# language is missed by the other pass, so both are run and merged.
|
|
117
|
+
# Only the mapped entity types are requested: the other recognizers
|
|
118
|
+
# (URL among them, slow on text with many dots) would be discarded.
|
|
114
119
|
raw = []
|
|
115
120
|
for lang in self.languages:
|
|
116
|
-
raw.extend(self._analyzer.analyze(text=text, language=lang))
|
|
121
|
+
raw.extend(self._analyzer.analyze(text=text, language=lang, entities=_ENTITIES))
|
|
117
122
|
|
|
118
123
|
spans: list[tuple[int, int, str, str]] = []
|
|
119
124
|
for r in raw:
|
|
@@ -16,17 +16,17 @@
|
|
|
16
16
|
|
|
17
17
|
Simple header-based authentication using constant-time comparison.
|
|
18
18
|
Supports both ``X-API-Key`` and ``Authorization: Bearer`` headers.
|
|
19
|
+
|
|
20
|
+
Cookies are never accepted here. The bundled dashboard's browser session is
|
|
21
|
+
handled by the proxy itself, which admits it only on the read-only dashboard
|
|
22
|
+
API (see ``admina.proxy.dashboard_session``).
|
|
19
23
|
"""
|
|
20
24
|
|
|
21
25
|
from __future__ import annotations
|
|
22
26
|
|
|
23
|
-
import base64
|
|
24
|
-
import hashlib
|
|
25
|
-
import hmac
|
|
26
27
|
import logging
|
|
27
28
|
import os
|
|
28
29
|
import secrets
|
|
29
|
-
import time
|
|
30
30
|
from typing import Any
|
|
31
31
|
|
|
32
32
|
from admina.plugins.base import BaseAuthProvider
|
|
@@ -94,12 +94,13 @@ class APIKeyAuthProvider(BaseAuthProvider):
|
|
|
94
94
|
if path in self._exempt_paths:
|
|
95
95
|
return {"user_id": "anonymous", "roles": ["public"], "metadata": {}}
|
|
96
96
|
|
|
97
|
-
# Accept
|
|
97
|
+
# Accept only the raw API key from X-API-Key or Authorization: Bearer.
|
|
98
98
|
provided = self._extract_key(request)
|
|
99
|
-
|
|
99
|
+
# Compare bytes: compare_digest raises TypeError on non-ASCII str.
|
|
100
|
+
if provided and secrets.compare_digest(
|
|
101
|
+
provided.encode("utf-8"), self._api_key.encode("utf-8")
|
|
102
|
+
):
|
|
100
103
|
return {"user_id": "api_key_user", "roles": ["authenticated"], "metadata": {}}
|
|
101
|
-
if self._verify_session_cookie(self._extract_session_cookie(request)):
|
|
102
|
-
return {"user_id": "dashboard_session", "roles": ["authenticated"], "metadata": {}}
|
|
103
104
|
raise PermissionError("Invalid or missing API key")
|
|
104
105
|
|
|
105
106
|
async def authorize(
|
|
@@ -128,12 +129,7 @@ class APIKeyAuthProvider(BaseAuthProvider):
|
|
|
128
129
|
|
|
129
130
|
@staticmethod
|
|
130
131
|
def _extract_key(request: Any) -> str:
|
|
131
|
-
"""Extract the raw API key from request headers (X-API-Key or Bearer).
|
|
132
|
-
|
|
133
|
-
The admina_session cookie is NOT extracted here — it carries a signed
|
|
134
|
-
token, not the raw key, and is handled separately by
|
|
135
|
-
``_extract_session_cookie`` + ``_verify_session_cookie``.
|
|
136
|
-
"""
|
|
132
|
+
"""Extract the raw API key from request headers (X-API-Key or Bearer)."""
|
|
137
133
|
if isinstance(request, dict):
|
|
138
134
|
headers = request.get("headers", {})
|
|
139
135
|
else:
|
|
@@ -145,36 +141,3 @@ class APIKeyAuthProvider(BaseAuthProvider):
|
|
|
145
141
|
if auth.startswith("Bearer "):
|
|
146
142
|
key = auth.removeprefix("Bearer ").strip()
|
|
147
143
|
return key
|
|
148
|
-
|
|
149
|
-
@staticmethod
|
|
150
|
-
def _extract_session_cookie(request: Any) -> str:
|
|
151
|
-
"""Extract the admina_session cookie value from a request."""
|
|
152
|
-
if isinstance(request, dict):
|
|
153
|
-
cookies = request.get("cookies", {})
|
|
154
|
-
else:
|
|
155
|
-
cookies = dict(getattr(request, "cookies", {}))
|
|
156
|
-
return cookies.get("admina_session", "")
|
|
157
|
-
|
|
158
|
-
def _verify_session_cookie(self, token: str) -> bool:
|
|
159
|
-
"""Verify a signed admina_session cookie against the configured key.
|
|
160
|
-
|
|
161
|
-
Mirrors the proxy's dashboard token format (HMAC-SHA256 over the
|
|
162
|
-
expiry). Replicated locally so this plugin does not depend on the
|
|
163
|
-
proxy package.
|
|
164
|
-
"""
|
|
165
|
-
if not self._api_key or not token:
|
|
166
|
-
return False
|
|
167
|
-
try:
|
|
168
|
-
raw = base64.urlsafe_b64decode(token.encode("ascii")).decode("utf-8")
|
|
169
|
-
payload, sig = raw.rsplit(".", 1)
|
|
170
|
-
except (ValueError, UnicodeDecodeError):
|
|
171
|
-
return False
|
|
172
|
-
expected = hmac.new(
|
|
173
|
-
self._api_key.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256
|
|
174
|
-
).hexdigest()
|
|
175
|
-
if not secrets.compare_digest(sig, expected):
|
|
176
|
-
return False
|
|
177
|
-
try:
|
|
178
|
-
return int(time.time()) < int(payload)
|
|
179
|
-
except ValueError:
|
|
180
|
-
return False
|