shadowshield 0.6.0__tar.gz → 0.6.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.
- {shadowshield-0.6.0 → shadowshield-0.6.2}/PKG-INFO +50 -7
- {shadowshield-0.6.0 → shadowshield-0.6.2}/README.md +47 -6
- {shadowshield-0.6.0 → shadowshield-0.6.2}/docs/BENCHMARKS.md +57 -4
- {shadowshield-0.6.0 → shadowshield-0.6.2}/docs/PRODUCTION_READINESS.md +42 -12
- shadowshield-0.6.2/docs/RELEASING.md +109 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/docs/index.md +1 -1
- {shadowshield-0.6.0 → shadowshield-0.6.2}/pyproject.toml +3 -1
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/__init__.py +1 -1
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/_security.py +31 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/cli.py +95 -6
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/control.py +472 -77
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/core/shield.py +24 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/detectors/base.py +13 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/detectors/exfiltration.py +11 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/detectors/jailbreak.py +30 -11
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/detectors/prompt_injection.py +23 -1
- shadowshield-0.6.2/src/shadowshield/detectors/transformer.py +219 -0
- shadowshield-0.6.2/src/shadowshield/detectors/vector.py +254 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/eval/__init__.py +2 -0
- shadowshield-0.6.2/src/shadowshield/eval/data/generalization_benchmark_v1.jsonl +30 -0
- shadowshield-0.6.2/src/shadowshield/eval/data/generalization_benchmark_v2.jsonl +20 -0
- shadowshield-0.6.2/src/shadowshield/eval/data/generalization_benchmark_v3.jsonl +40 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/eval/dataset.py +21 -6
- shadowshield-0.6.2/src/shadowshield/reporter.py +250 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/server.py +13 -1
- shadowshield-0.6.2/tests/test_adversarial.py +19 -0
- shadowshield-0.6.2/tests/test_cli.py +107 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/tests/test_control.py +412 -14
- {shadowshield-0.6.0 → shadowshield-0.6.2}/tests/test_detectors.py +51 -1
- shadowshield-0.6.2/tests/test_generalization_benchmarks.py +138 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/tests/test_server.py +100 -0
- shadowshield-0.6.2/tests/test_site_security.py +107 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/tests/test_spans.py +1 -1
- shadowshield-0.6.2/tests/test_telemetry.py +324 -0
- shadowshield-0.6.2/tests/test_transformer.py +345 -0
- shadowshield-0.6.2/tests/test_vector.py +478 -0
- shadowshield-0.6.0/docs/RELEASING.md +0 -66
- shadowshield-0.6.0/src/shadowshield/detectors/transformer.py +0 -115
- shadowshield-0.6.0/src/shadowshield/detectors/vector.py +0 -137
- shadowshield-0.6.0/src/shadowshield/reporter.py +0 -151
- shadowshield-0.6.0/tests/test_adversarial.py +0 -25
- shadowshield-0.6.0/tests/test_cli.py +0 -48
- shadowshield-0.6.0/tests/test_telemetry.py +0 -112
- shadowshield-0.6.0/tests/test_transformer.py +0 -129
- shadowshield-0.6.0/tests/test_vector.py +0 -159
- {shadowshield-0.6.0 → shadowshield-0.6.2}/.gitignore +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/LICENSE +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/docs/CODE_REVIEW.md +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/docs/COMPARISON.md +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/docs/MARKET_LANDSCAPE.md +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/docs/NEXT_STEPS.md +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/docs/OWASP_LLM_TOP10.md +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/docs/PLAN_REVIEW.md +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/docs/REPORTER_SDK_SPEC.md +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/docs/SAAS_STRATEGY.md +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/docs/UPGRADE_OPPORTUNITIES.md +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/docs/configuration.md +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/docs/detectors.md +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/docs/plugins.md +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/docs/research/LANDSCAPE.md +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/docs/security-model.md +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/examples/agentic_security.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/examples/custom_detector.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/examples/langchain_integration.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/examples/openai_integration.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/examples/quickstart.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/config/__init__.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/config/default.yaml +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/core/__init__.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/core/canary.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/core/config.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/core/coverage.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/core/engine.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/core/policy.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/core/session.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/core/telemetry.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/core/types.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/detectors/__init__.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/detectors/alignment.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/detectors/anomaly.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/detectors/canary.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/detectors/data/attack_corpus.txt +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/detectors/encoding.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/detectors/llm_check.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/detectors/pii.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/eval/data/adversarial_benchmark.jsonl +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/eval/data/builtin_benchmark.jsonl +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/eval/harness.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/integrations/__init__.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/integrations/agentdojo.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/integrations/mcp.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/middleware/__init__.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/middleware/base.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/middleware/decorators.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/middleware/langchain.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/middleware/openai.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/plugins/__init__.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/plugins/base.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/plugins/manager.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/py.typed +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/responders/__init__.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/responders/base.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/responders/blocker.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/responders/isolator.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/responders/rate_limiter.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/responders/sanitizer.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/static/dashboard.html +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/utils/__init__.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/utils/logging.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/utils/scoring.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/src/shadowshield/utils/text.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/tests/__init__.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/tests/test_agentic.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/tests/test_config.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/tests/test_http_security.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/tests/test_mcp_guard.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/tests/test_middleware.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/tests/test_multilingual.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/tests/test_pii_backends.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/tests/test_policy.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/tests/test_production.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/tests/test_prompt_injection.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/tests/test_responders.py +0 -0
- {shadowshield-0.6.0 → shadowshield-0.6.2}/tests/test_shield.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shadowshield
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: Unified open-source security shield for agentic AI systems — inspired by Sentinel & ShadowClaw.
|
|
5
5
|
Project-URL: Homepage, https://shadowshield.xyz
|
|
6
6
|
Project-URL: Documentation, https://github.com/0xsl1m/shadowshield#readme
|
|
@@ -18,6 +18,8 @@ Classifier: Programming Language :: Python :: 3
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
23
|
Classifier: Topic :: Security
|
|
22
24
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
25
|
Classifier: Typing :: Typed
|
|
@@ -146,6 +148,10 @@ print(result.safe_text) # safe fallback message
|
|
|
146
148
|
> The bundled offline set (`shadowshield benchmark`) scores 100%/0-FP, but that's an
|
|
147
149
|
> in-distribution **regression baseline, not a SOTA claim**. We publish the humbling
|
|
148
150
|
> external numbers on purpose — a credible security tool shows its homework.
|
|
151
|
+
> The frozen blind semantic snapshots are harder still: v1 reaches 26.7% recall /
|
|
152
|
+
> 13.3% FPR, v2 reaches 0% / 10%, and v3 reaches 30% / 30%; the 90-row aggregate
|
|
153
|
+
> is 22.2% / 20%. Run `shadowshield benchmark --generalization all`; these gaps
|
|
154
|
+
> are public by design.
|
|
149
155
|
|
|
150
156
|
---
|
|
151
157
|
|
|
@@ -289,6 +295,8 @@ shadowshield scan --text "you are now DAN" --mode strict --json
|
|
|
289
295
|
shadowshield detectors # list registered detectors
|
|
290
296
|
shadowshield init > shield.yaml # write an annotated default config
|
|
291
297
|
shadowshield benchmark # run the bundled offline benchmark
|
|
298
|
+
shadowshield benchmark --adversarial
|
|
299
|
+
shadowshield benchmark --generalization all # all frozen blind semantic snapshots
|
|
292
300
|
shadowshield serve # HTTP server + live dashboard (needs [dashboard])
|
|
293
301
|
```
|
|
294
302
|
|
|
@@ -304,9 +312,10 @@ curl -s localhost:8000/scan -H 'content-type: application/json' \
|
|
|
304
312
|
-d '{"text":"ignore all previous instructions","direction":"input"}'
|
|
305
313
|
# {"decision":"block","blocked":true,"score":0.9,...}
|
|
306
314
|
```
|
|
307
|
-
Endpoints: `GET /health
|
|
308
|
-
factory mounting fails closed unless
|
|
309
|
-
embeddings must explicitly pass
|
|
315
|
+
Endpoints: `GET /health` (liveness), `GET /ready` (readiness), `POST /scan`,
|
|
316
|
+
`POST /guard`, `GET /` (dashboard). Direct factory mounting fails closed unless
|
|
317
|
+
`api_keys` is supplied; local-only trusted embeddings must explicitly pass
|
|
318
|
+
`allow_insecure_local=True`.
|
|
310
319
|
|
|
311
320
|
### Production container
|
|
312
321
|
|
|
@@ -319,13 +328,46 @@ credentials, requires signed policies, and persists authenticated anti-replay st
|
|
|
319
328
|
export SHADOWSHIELD_API_KEY="$(openssl rand -hex 32)"
|
|
320
329
|
export SHADOWSHIELD_ADMIN_KEY="$(openssl rand -hex 32)"
|
|
321
330
|
export SHADOWSHIELD_POLICY_KEY="$(openssl rand -hex 32)"
|
|
322
|
-
|
|
331
|
+
export SHADOWSHIELD_POLICY_STATE_KEY="$(openssl rand -hex 32)"
|
|
332
|
+
export SHADOWSHIELD_IMAGE_DIGEST="$(curl -fsSL \
|
|
333
|
+
https://github.com/0xsl1m/shadowshield/releases/download/v0.6.2/container-digest.txt)"
|
|
334
|
+
docker compose pull
|
|
335
|
+
docker compose up -d
|
|
323
336
|
```
|
|
324
337
|
|
|
325
|
-
|
|
338
|
+
The release workflow scans that exact image, publishes it to GHCR, signs SLSA
|
|
339
|
+
provenance plus the CycloneDX SBOM with ephemeral GitHub OIDC/Sigstore identity,
|
|
340
|
+
verifies the registry-attached source/workflow claims and an anonymous digest
|
|
341
|
+
pull, and attaches the digest plus SBOM to the matching GitHub Release. Stable
|
|
342
|
+
release tags are accepted only from an exact green `main` commit. All four
|
|
343
|
+
secrets must be independent. Terminate TLS at a trusted ingress before exposing
|
|
344
|
+
it beyond localhost. See the
|
|
326
345
|
[production-readiness roadmap](docs/PRODUCTION_READINESS.md) for launch gates,
|
|
327
346
|
known scale limits, and the operator checklist.
|
|
328
347
|
|
|
348
|
+
Upgrading a control-plane volume from 0.6.0 requires an offline re-key because
|
|
349
|
+
0.6.0 authenticated durable state with the policy-signing key:
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
# Load the existing scan/admin keys first so the new Compose file can resolve.
|
|
353
|
+
export SHADOWSHIELD_IMAGE_DIGEST="$(curl -fsSL \
|
|
354
|
+
https://github.com/0xsl1m/shadowshield/releases/download/v0.6.2/container-digest.txt)"
|
|
355
|
+
export SHADOWSHIELD_POLICY_KEY="<existing-0.6.0-policy-key>"
|
|
356
|
+
export SHADOWSHIELD_POLICY_STATE_KEY="$(openssl rand -hex 32)"
|
|
357
|
+
# Stop every writer and snapshot the volume before running the migration.
|
|
358
|
+
docker compose stop shadowshield
|
|
359
|
+
docker compose run --rm --no-deps shadowshield \
|
|
360
|
+
shadowshield migrate-policy-state --path /var/lib/shadowshield/policy-state.json
|
|
361
|
+
# Preserve the reported .pre-0.6.1.bak file, then start 0.6.2.
|
|
362
|
+
docker compose up -d
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
The command verifies the old MAC and restorable policy, creates an exclusive
|
|
366
|
+
backup, and atomically re-MACs the state. Do not delete the old state to bypass
|
|
367
|
+
migration: that discards replay history and the last-known-good policy. For a
|
|
368
|
+
non-container install, run the same `shadowshield migrate-policy-state` command
|
|
369
|
+
directly against the stopped service's state path.
|
|
370
|
+
|
|
329
371
|
---
|
|
330
372
|
|
|
331
373
|
## Agentic & advanced features
|
|
@@ -567,7 +609,8 @@ shadowshield serve --control --api-key SECRET # require X-API-Key / Bearer
|
|
|
567
609
|
```bash
|
|
568
610
|
shadowshield schema # config JSON Schema (editor/CI validation)
|
|
569
611
|
shadowshield owasp # OWASP LLM Top 10 (2025) coverage map
|
|
570
|
-
shadowshield benchmark --adversarial
|
|
612
|
+
shadowshield benchmark --adversarial # curated regression set
|
|
613
|
+
shadowshield benchmark --generalization all # all frozen blind semantic snapshots
|
|
571
614
|
```
|
|
572
615
|
|
|
573
616
|
## Documentation
|
|
@@ -75,6 +75,10 @@ print(result.safe_text) # safe fallback message
|
|
|
75
75
|
> The bundled offline set (`shadowshield benchmark`) scores 100%/0-FP, but that's an
|
|
76
76
|
> in-distribution **regression baseline, not a SOTA claim**. We publish the humbling
|
|
77
77
|
> external numbers on purpose — a credible security tool shows its homework.
|
|
78
|
+
> The frozen blind semantic snapshots are harder still: v1 reaches 26.7% recall /
|
|
79
|
+
> 13.3% FPR, v2 reaches 0% / 10%, and v3 reaches 30% / 30%; the 90-row aggregate
|
|
80
|
+
> is 22.2% / 20%. Run `shadowshield benchmark --generalization all`; these gaps
|
|
81
|
+
> are public by design.
|
|
78
82
|
|
|
79
83
|
---
|
|
80
84
|
|
|
@@ -218,6 +222,8 @@ shadowshield scan --text "you are now DAN" --mode strict --json
|
|
|
218
222
|
shadowshield detectors # list registered detectors
|
|
219
223
|
shadowshield init > shield.yaml # write an annotated default config
|
|
220
224
|
shadowshield benchmark # run the bundled offline benchmark
|
|
225
|
+
shadowshield benchmark --adversarial
|
|
226
|
+
shadowshield benchmark --generalization all # all frozen blind semantic snapshots
|
|
221
227
|
shadowshield serve # HTTP server + live dashboard (needs [dashboard])
|
|
222
228
|
```
|
|
223
229
|
|
|
@@ -233,9 +239,10 @@ curl -s localhost:8000/scan -H 'content-type: application/json' \
|
|
|
233
239
|
-d '{"text":"ignore all previous instructions","direction":"input"}'
|
|
234
240
|
# {"decision":"block","blocked":true,"score":0.9,...}
|
|
235
241
|
```
|
|
236
|
-
Endpoints: `GET /health
|
|
237
|
-
factory mounting fails closed unless
|
|
238
|
-
embeddings must explicitly pass
|
|
242
|
+
Endpoints: `GET /health` (liveness), `GET /ready` (readiness), `POST /scan`,
|
|
243
|
+
`POST /guard`, `GET /` (dashboard). Direct factory mounting fails closed unless
|
|
244
|
+
`api_keys` is supplied; local-only trusted embeddings must explicitly pass
|
|
245
|
+
`allow_insecure_local=True`.
|
|
239
246
|
|
|
240
247
|
### Production container
|
|
241
248
|
|
|
@@ -248,13 +255,46 @@ credentials, requires signed policies, and persists authenticated anti-replay st
|
|
|
248
255
|
export SHADOWSHIELD_API_KEY="$(openssl rand -hex 32)"
|
|
249
256
|
export SHADOWSHIELD_ADMIN_KEY="$(openssl rand -hex 32)"
|
|
250
257
|
export SHADOWSHIELD_POLICY_KEY="$(openssl rand -hex 32)"
|
|
251
|
-
|
|
258
|
+
export SHADOWSHIELD_POLICY_STATE_KEY="$(openssl rand -hex 32)"
|
|
259
|
+
export SHADOWSHIELD_IMAGE_DIGEST="$(curl -fsSL \
|
|
260
|
+
https://github.com/0xsl1m/shadowshield/releases/download/v0.6.2/container-digest.txt)"
|
|
261
|
+
docker compose pull
|
|
262
|
+
docker compose up -d
|
|
252
263
|
```
|
|
253
264
|
|
|
254
|
-
|
|
265
|
+
The release workflow scans that exact image, publishes it to GHCR, signs SLSA
|
|
266
|
+
provenance plus the CycloneDX SBOM with ephemeral GitHub OIDC/Sigstore identity,
|
|
267
|
+
verifies the registry-attached source/workflow claims and an anonymous digest
|
|
268
|
+
pull, and attaches the digest plus SBOM to the matching GitHub Release. Stable
|
|
269
|
+
release tags are accepted only from an exact green `main` commit. All four
|
|
270
|
+
secrets must be independent. Terminate TLS at a trusted ingress before exposing
|
|
271
|
+
it beyond localhost. See the
|
|
255
272
|
[production-readiness roadmap](docs/PRODUCTION_READINESS.md) for launch gates,
|
|
256
273
|
known scale limits, and the operator checklist.
|
|
257
274
|
|
|
275
|
+
Upgrading a control-plane volume from 0.6.0 requires an offline re-key because
|
|
276
|
+
0.6.0 authenticated durable state with the policy-signing key:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
# Load the existing scan/admin keys first so the new Compose file can resolve.
|
|
280
|
+
export SHADOWSHIELD_IMAGE_DIGEST="$(curl -fsSL \
|
|
281
|
+
https://github.com/0xsl1m/shadowshield/releases/download/v0.6.2/container-digest.txt)"
|
|
282
|
+
export SHADOWSHIELD_POLICY_KEY="<existing-0.6.0-policy-key>"
|
|
283
|
+
export SHADOWSHIELD_POLICY_STATE_KEY="$(openssl rand -hex 32)"
|
|
284
|
+
# Stop every writer and snapshot the volume before running the migration.
|
|
285
|
+
docker compose stop shadowshield
|
|
286
|
+
docker compose run --rm --no-deps shadowshield \
|
|
287
|
+
shadowshield migrate-policy-state --path /var/lib/shadowshield/policy-state.json
|
|
288
|
+
# Preserve the reported .pre-0.6.1.bak file, then start 0.6.2.
|
|
289
|
+
docker compose up -d
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
The command verifies the old MAC and restorable policy, creates an exclusive
|
|
293
|
+
backup, and atomically re-MACs the state. Do not delete the old state to bypass
|
|
294
|
+
migration: that discards replay history and the last-known-good policy. For a
|
|
295
|
+
non-container install, run the same `shadowshield migrate-policy-state` command
|
|
296
|
+
directly against the stopped service's state path.
|
|
297
|
+
|
|
258
298
|
---
|
|
259
299
|
|
|
260
300
|
## Agentic & advanced features
|
|
@@ -496,7 +536,8 @@ shadowshield serve --control --api-key SECRET # require X-API-Key / Bearer
|
|
|
496
536
|
```bash
|
|
497
537
|
shadowshield schema # config JSON Schema (editor/CI validation)
|
|
498
538
|
shadowshield owasp # OWASP LLM Top 10 (2025) coverage map
|
|
499
|
-
shadowshield benchmark --adversarial
|
|
539
|
+
shadowshield benchmark --adversarial # curated regression set
|
|
540
|
+
shadowshield benchmark --generalization all # all frozen blind semantic snapshots
|
|
500
541
|
```
|
|
501
542
|
|
|
502
543
|
## Documentation
|
|
@@ -10,6 +10,13 @@ one** — a guard that blocks everything has perfect recall and is useless.
|
|
|
10
10
|
```bash
|
|
11
11
|
# Bundled offline benchmark (no network, no extra deps)
|
|
12
12
|
shadowshield benchmark
|
|
13
|
+
shadowshield benchmark --adversarial
|
|
14
|
+
|
|
15
|
+
# Independently authored blind semantic snapshots
|
|
16
|
+
shadowshield benchmark --generalization v1
|
|
17
|
+
shadowshield benchmark --generalization v2
|
|
18
|
+
shadowshield benchmark --generalization v3
|
|
19
|
+
shadowshield benchmark --generalization all
|
|
13
20
|
|
|
14
21
|
# External public dataset (needs the datasets extra)
|
|
15
22
|
pip install "shadowshield[datasets]"
|
|
@@ -30,9 +37,54 @@ negatives). `balanced` mode:
|
|
|
30
37
|
|
|
31
38
|
**This is a regression baseline and a smoke test — NOT a claim of real-world
|
|
32
39
|
accuracy.** 100% on our own set just means we don't regress on the attack
|
|
33
|
-
catalogue we curated. The
|
|
40
|
+
catalogue we curated. The blind and external numbers below are the real constraint.
|
|
41
|
+
|
|
42
|
+
## 2. Curated adversarial regression set
|
|
43
|
+
|
|
44
|
+
The 36-row adversarial catalogue includes obfuscation, multilingual and indirect
|
|
45
|
+
attacks, plus benign trigger-heavy counterexamples. It improved from
|
|
46
|
+
15/2/16/3 to 18/0/18/0 (TP/FP/TN/FN):
|
|
47
|
+
|
|
48
|
+
| recall | FPR | precision | F1 |
|
|
49
|
+
|---:|---:|---:|---:|
|
|
50
|
+
| 100% | 0% | 100% | 100% |
|
|
51
|
+
|
|
52
|
+
This is still a curated regression set. The signatures were developed with these
|
|
53
|
+
cases visible, so its perfect score is not a generalization claim.
|
|
54
|
+
|
|
55
|
+
## 3. Blind semantic snapshots (the anti-gaming result)
|
|
56
|
+
|
|
57
|
+
Three balanced snapshots were authored independently without detector or regex
|
|
58
|
+
context, then frozen. They deliberately pair attacks with benign text containing
|
|
59
|
+
similar roleplay, authority, debug, and policy vocabulary.
|
|
60
|
+
|
|
61
|
+
Isolation protocol: the v1/v2 authoring tasks received only the five semantic
|
|
62
|
+
category names, required row/balance counts, and a request for attack/benign
|
|
63
|
+
contrast pairs. The v3 task received modality coverage requirements but no
|
|
64
|
+
detector source, signatures, or current predictions. The files were frozen before
|
|
65
|
+
detector tuning and are integrity-pinned in tests:
|
|
66
|
+
v1 SHA-256 `b3281ba1a42d266bb930bbb41943016d47b38dbc822ff7cff5131f3448a0248f`;
|
|
67
|
+
v2 SHA-256 `aa8b8c81c00a55bb65180e15ff743b6241d24845b3886e8e60b52b9b23db47fa`;
|
|
68
|
+
v3 SHA-256 `2285031e8143572311a522a4b6ec1a39c96a34ac2b42785f17818e8a145342bf`.
|
|
69
|
+
|
|
70
|
+
| snapshot | rows | TP/FP/TN/FN | recall | FPR | balanced accuracy |
|
|
71
|
+
|---|---:|---:|---:|---:|---:|
|
|
72
|
+
| v1 | 30 | 4/2/13/11 | 26.7% | 13.3% | 56.7% |
|
|
73
|
+
| v2 | 20 | 0/1/9/10 | 0% | 10% | 45% |
|
|
74
|
+
| v3 | 40 | 6/6/14/14 | 30% | 30% | 50% |
|
|
75
|
+
| **aggregate** | **90** | **10/9/36/35** | **22.2%** | **20%** | **51.1%** |
|
|
76
|
+
|
|
77
|
+
The v3 snapshot was opened only after a detector candidate and acceptance bar
|
|
78
|
+
were frozen. That candidate raised aggregate recall to 55.6% but failed the
|
|
79
|
+
predeclared false-positive and balanced-accuracy gates (24.4% FPR; 65.6%
|
|
80
|
+
balanced accuracy), so it was discarded in full. No v3-driven detector tuning
|
|
81
|
+
ships in 0.6.2; the table records the unchanged released core.
|
|
82
|
+
|
|
83
|
+
These snapshots expose the core deterministic tier's semantic-generalization
|
|
84
|
+
ceiling and are development evidence for a fresh-corpus classifier/conjunction
|
|
85
|
+
tranche, not numbers to hide or a set to tune against.
|
|
34
86
|
|
|
35
|
-
##
|
|
87
|
+
## 4. External: `deepset/prompt-injections` (out-of-distribution)
|
|
36
88
|
|
|
37
89
|
The field's standard public smoke set (662 rows, English + German, diverse
|
|
38
90
|
phrasings). This is where in-distribution scores collapse — and ours do too.
|
|
@@ -72,7 +124,7 @@ via `use_transformer="meta-llama/Llama-Prompt-Guard-2-22M"`. Note: the Prompt-Gu
|
|
|
72
124
|
models are **gated** on HuggingFace — accept the license and run `huggingface-cli
|
|
73
125
|
login` (or set `HF_TOKEN`) first. The default ProtectAI model needs no token.
|
|
74
126
|
|
|
75
|
-
##
|
|
127
|
+
## 5. Interpretation & roadmap
|
|
76
128
|
|
|
77
129
|
- **Use the deterministic tiers** for cheap, explainable, obfuscation-aware,
|
|
78
130
|
zero-false-positive catches and for everything the classifier can't do
|
|
@@ -90,6 +142,7 @@ it needs `pip install agentdojo` and an LLM API key; the adapter and a
|
|
|
90
142
|
standalone `scan_messages_for_injection` helper are tested and ready. Publishing
|
|
91
143
|
the ASR-at-fixed-utility number is the next milestone.
|
|
92
144
|
|
|
93
|
-
>
|
|
145
|
+
> External model numbers measured 2026-06-12; curated/blind snapshots measured
|
|
146
|
+
> 2026-07-25 on CPU. Latency is hardware-dependent; the
|
|
94
147
|
> classifier adds tens of ms/scan on CPU, the vector tier ~20 ms, vs. sub-ms for
|
|
95
148
|
> the deterministic tiers.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Production readiness roadmap
|
|
2
2
|
|
|
3
|
-
**Baseline:** ShadowShield 0.6.
|
|
3
|
+
**Baseline:** ShadowShield 0.6.2 · **Audit date:** 2026-07-25
|
|
4
4
|
|
|
5
5
|
This roadmap is the release gate for the Python library and the optional HTTP
|
|
6
6
|
control plane. A checked item is implemented and locally verified; unchecked
|
|
@@ -10,12 +10,12 @@ items require an explicit operator or maintainer decision before public launch.
|
|
|
10
10
|
|
|
11
11
|
| Area | State | Evidence / remaining action |
|
|
12
12
|
|---|---|---|
|
|
13
|
-
| Correctness | Ready |
|
|
13
|
+
| Correctness | Ready | 290+ unit/integration cases; strict mypy; Ruff lint/format; CodeQL extended scanning |
|
|
14
14
|
| Packaging | Ready | Isolated sdist/wheel build, Twine metadata check, installed-wheel CI smoke test |
|
|
15
15
|
| Runtime security | Ready with configuration | Early API-key/Bearer auth, body limits, restricted CORS, immutable protection floor, non-root read-only container |
|
|
16
|
-
| Supply chain | Ready |
|
|
16
|
+
| Supply chain | Ready | Repository-enforced Action SHA pins, PyPI OIDC, exact-green-main release gates, pre-push image vulnerability gate, anonymous GHCR digest pull, registry-verified SLSA and CycloneDX attestations |
|
|
17
17
|
| Observability | Ready for single process | Content-free telemetry, Prometheus endpoint, bounded in-memory event feed |
|
|
18
|
-
| Detection quality | Beta |
|
|
18
|
+
| Detection quality | Beta | Curated adversarial: 100%/0% FPR; blind v1–v3 aggregate: 22.2%/20%; v3: 30%/30%; deepset core: 23.3%/0% |
|
|
19
19
|
| Scale / HA | Not yet | Counters, events, configuration, and rate limits are process-local |
|
|
20
20
|
| Streaming | Not yet | Outputs must be buffered before scanning |
|
|
21
21
|
|
|
@@ -38,28 +38,58 @@ items require an explicit operator or maintainer decision before public launch.
|
|
|
38
38
|
signed last-known-good policy state.
|
|
39
39
|
- [x] Bound scan concurrency and ensure permanently hung judges cannot block shutdown.
|
|
40
40
|
- [x] Pin every third-party GitHub Action to a reviewed commit SHA.
|
|
41
|
+
- [x] Separate policy-state authentication from policy signing and reject all
|
|
42
|
+
pairwise-equal production credentials.
|
|
43
|
+
- [x] Add a verified offline 0.6.0 policy-state migration and reject state that
|
|
44
|
+
would exceed the bounded restart parser before changing the live policy.
|
|
45
|
+
- [x] Add non-loading readiness probes and explicit fail-fast detector warmup.
|
|
46
|
+
- [x] Make Transformer loading and Vector mutation single-flight/transactional.
|
|
47
|
+
- [x] Bound Reporter flush/retry/close lifecycle under concurrency.
|
|
48
|
+
- [x] Pin the base image by digest, gate the exact release image on Trivy, publish
|
|
49
|
+
it to GHCR, verify an anonymous digest pull, and attach its immutable digest
|
|
50
|
+
plus CycloneDX SBOM to the release.
|
|
51
|
+
- [x] Sign release-image SLSA provenance and the CycloneDX SBOM with ephemeral
|
|
52
|
+
GitHub OIDC/Sigstore identity, then verify repository, workflow, runner, and
|
|
53
|
+
source-commit claims from the OCI registry before completing the release.
|
|
54
|
+
- [x] Reject release publication unless a stable, non-prerelease tag matches the
|
|
55
|
+
package version, resolves to `main`, and has a successful exact-SHA `main` CI run.
|
|
56
|
+
- [x] Reject symlink/non-regular durable policy-state files, use unpredictable
|
|
57
|
+
exclusive temporary files, and revision-check reads, migrations, and replaces.
|
|
58
|
+
- [x] Coalesce concurrent failed Transformer/Vector load generations while giving
|
|
59
|
+
every waiter a fresh bounded exception traceback and allowing a later retry.
|
|
60
|
+
- [x] Enable Dependabot security updates, extended CodeQL scanning, secret
|
|
61
|
+
push-protection, and repository-level enforcement of full-SHA Action pins.
|
|
62
|
+
- [x] Publish a third independently authored blind snapshot and reject—not
|
|
63
|
+
tune—the first frozen candidate when it missed the predeclared FPR/accuracy bar.
|
|
41
64
|
|
|
42
65
|
## P1 — launch procedure (operator-owned)
|
|
43
66
|
|
|
44
|
-
- [ ] Set independent high-entropy `SHADOWSHIELD_API_KEY`,
|
|
67
|
+
- [ ] Set independent high-entropy `SHADOWSHIELD_API_KEY`,
|
|
68
|
+
`SHADOWSHIELD_ADMIN_KEY`, `SHADOWSHIELD_POLICY_KEY`, and
|
|
69
|
+
`SHADOWSHIELD_POLICY_STATE_KEY` values; terminate TLS at a trusted ingress.
|
|
70
|
+
- [ ] When upgrading a 0.6.0 durable volume, stop all writers, snapshot the
|
|
71
|
+
volume, run `shadowshield migrate-policy-state`, retain its verified backup,
|
|
72
|
+
and confirm the restored policy/version before accepting traffic.
|
|
45
73
|
- [ ] Set an explicit CORS allowlist or leave CORS disabled.
|
|
46
74
|
- [ ] Run the adversarial and application-specific eval sets in permissive/shadow mode.
|
|
47
75
|
- [ ] Choose thresholds from measured false-positive cost; record the accepted baseline.
|
|
48
76
|
- [ ] Connect Prometheus and content-free telemetry; alert on error, block-rate, and latency shifts.
|
|
49
77
|
- [ ] Load-test representative payload sizes and concurrency on the target instance class.
|
|
50
|
-
- [ ]
|
|
78
|
+
- [ ] Record the deployed image digest and CI SBOM, then rehearse backup/restore
|
|
79
|
+
and rollback against the target environment.
|
|
51
80
|
- [ ] Protect the PyPI GitHub Environment with required reviewers before publishing.
|
|
52
81
|
|
|
53
82
|
## P2 — next engineering milestones
|
|
54
83
|
|
|
55
84
|
1. Add a rolling-window streaming scanner that blocks before unsafe output is emitted.
|
|
56
85
|
2. Move rate limits, event history, and metrics aggregation to shared external stores for multi-worker HA.
|
|
57
|
-
3.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
86
|
+
3. Build a fresh independently sourced development corpus, then evaluate the
|
|
87
|
+
next detector candidate once on a new sealed snapshot and publish confidence intervals.
|
|
88
|
+
4. Add multi-hour soak and network fault-injection tests for reporters, judges, and hot policy updates.
|
|
89
|
+
5. Split the current administrator credential into narrower observe and mutation scopes.
|
|
90
|
+
6. Design cross-process model/index coordination and durable Reporter spooling.
|
|
91
|
+
7. Split unprivileged container build/scan from the write/OIDC publication job.
|
|
92
|
+
8. Refactor release evidence into a draft-first pipeline, then enable immutable releases.
|
|
63
93
|
|
|
64
94
|
## Go / no-go gates
|
|
65
95
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Releasing ShadowShield
|
|
2
|
+
|
|
3
|
+
ShadowShield publishes via **PyPI Trusted Publishing** (OpenID Connect from GitHub
|
|
4
|
+
Actions). No API token is ever created, stored, or pasted anywhere — PyPI trusts
|
|
5
|
+
this specific GitHub workflow directly. The pipeline is
|
|
6
|
+
[`publish.yml`](../.github/workflows/publish.yml).
|
|
7
|
+
|
|
8
|
+
The same GitHub Release also triggers
|
|
9
|
+
[`container-release.yml`](../.github/workflows/container-release.yml). It builds
|
|
10
|
+
one image from the release tag, generates a CycloneDX SBOM, rejects fixable
|
|
11
|
+
high/critical findings, pushes that exact scanned image to GHCR, signs SLSA
|
|
12
|
+
provenance and CycloneDX attestations through GitHub's Sigstore-backed OIDC
|
|
13
|
+
service, verifies the signed provenance and an anonymous pull by digest, and
|
|
14
|
+
attaches `container-digest.txt` plus the SBOM to the GitHub Release.
|
|
15
|
+
|
|
16
|
+
## One-time setup (PyPI account owner only)
|
|
17
|
+
|
|
18
|
+
This must be done in the PyPI web UI by the account that will own the project —
|
|
19
|
+
it cannot be automated.
|
|
20
|
+
|
|
21
|
+
1. Create a PyPI account (and enable 2FA): <https://pypi.org/account/register/>.
|
|
22
|
+
2. Go to **<https://pypi.org/manage/account/publishing/>** → "Add a new pending
|
|
23
|
+
publisher" and enter **exactly**:
|
|
24
|
+
- **PyPI Project Name:** `shadowshield`
|
|
25
|
+
- **Owner:** `0xsl1m`
|
|
26
|
+
- **Repository name:** `shadowshield`
|
|
27
|
+
- **Workflow name:** `publish.yml`
|
|
28
|
+
- **Environment name:** *(leave blank — the workflow declares no environment)*
|
|
29
|
+
3. Save. This registers a *pending publisher*; the PyPI project is created
|
|
30
|
+
automatically on the first successful publish.
|
|
31
|
+
|
|
32
|
+
> Optional hardening: create a GitHub Environment named `pypi` with required
|
|
33
|
+
> reviewers, add `environment: pypi` to the `publish` job, and set the same
|
|
34
|
+
> environment name in the PyPI publisher config. Then every publish needs manual
|
|
35
|
+
> approval in GitHub.
|
|
36
|
+
|
|
37
|
+
## One-time setup (GHCR package owner only)
|
|
38
|
+
|
|
39
|
+
GitHub may create a first-published container package as private. The release
|
|
40
|
+
workflow deliberately fails its anonymous-pull gate if that happens.
|
|
41
|
+
|
|
42
|
+
1. Let the first `Publish release container` run push the package.
|
|
43
|
+
2. If the anonymous-pull step fails, open the `shadowshield` package settings
|
|
44
|
+
under the `0xsl1m` account and change visibility to **Public**.
|
|
45
|
+
3. Re-run the failed workflow job. Do not publish the release digest to operators
|
|
46
|
+
until the anonymous pull and evidence-attachment steps are green.
|
|
47
|
+
|
|
48
|
+
## Cutting a release
|
|
49
|
+
|
|
50
|
+
Only release an exact commit whose PR and `main` CI checks are green:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# 1. bump the version in pyproject.toml + src/shadowshield/__init__.py, update CHANGELOG
|
|
54
|
+
# 2. merge through a reviewed PR and wait for main CI + site deployment
|
|
55
|
+
# 3. create the release/tag at that exact merge commit
|
|
56
|
+
merge_sha="$(git rev-parse origin/main)"
|
|
57
|
+
gh release create vX.Y.Z --target "$merge_sha" \
|
|
58
|
+
--title "ShadowShield X.Y.Z" --notes "..."
|
|
59
|
+
# -> PyPI publishing and exact-image scan/publish run independently
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Do not create the release while required checks are pending. A partial release
|
|
63
|
+
is recoverable by fixing the failed workflow and re-running it, but the PyPI
|
|
64
|
+
version itself cannot be replaced.
|
|
65
|
+
|
|
66
|
+
Both publication workflows independently reject non-stable tags, tag/source
|
|
67
|
+
version mismatches, commits outside `main`, and commits without a successful
|
|
68
|
+
exact-SHA `main` CI run. There is no manual-dispatch publication path; use
|
|
69
|
+
GitHub's **Re-run failed jobs** control to recover a failed release workflow.
|
|
70
|
+
|
|
71
|
+
## Verify
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip index versions shadowshield # should list the new version
|
|
75
|
+
pip install shadowshield==X.Y.Z # clean-env smoke test
|
|
76
|
+
python -c "import shadowshield as ss; print(ss.__version__)"
|
|
77
|
+
|
|
78
|
+
gh release download vX.Y.Z \
|
|
79
|
+
--pattern container-digest.txt --pattern shadowshield-sbom.cdx.json
|
|
80
|
+
digest="$(cat container-digest.txt)"
|
|
81
|
+
image="ghcr.io/0xsl1m/shadowshield@$digest"
|
|
82
|
+
docker pull "$image"
|
|
83
|
+
gh attestation verify "oci://$image" \
|
|
84
|
+
--repo 0xsl1m/shadowshield \
|
|
85
|
+
--bundle-from-oci \
|
|
86
|
+
--signer-workflow 0xsl1m/shadowshield/.github/workflows/container-release.yml \
|
|
87
|
+
--source-digest "$(git rev-list -n 1 vX.Y.Z)" \
|
|
88
|
+
--deny-self-hosted-runners
|
|
89
|
+
gh attestation verify "oci://$image" \
|
|
90
|
+
--repo 0xsl1m/shadowshield \
|
|
91
|
+
--bundle-from-oci \
|
|
92
|
+
--predicate-type https://cyclonedx.org/bom \
|
|
93
|
+
--signer-workflow 0xsl1m/shadowshield/.github/workflows/container-release.yml \
|
|
94
|
+
--source-digest "$(git rev-list -n 1 vX.Y.Z)" \
|
|
95
|
+
--deny-self-hosted-runners
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Notes
|
|
99
|
+
|
|
100
|
+
- **Versions are immutable.** A published version can never be re-uploaded, only
|
|
101
|
+
*yanked*. Always confirm `twine check` is green (CI does this) before releasing.
|
|
102
|
+
- Deploy the container by the exact `image@sha256` value in
|
|
103
|
+
`container-digest.txt`, never by a mutable version tag or a local Compose
|
|
104
|
+
rebuild. The release SBOM describes that exact pre-push-scanned image.
|
|
105
|
+
- The default install stays lightweight; the ML/vector/PII/dataset stacks are
|
|
106
|
+
optional extras (see `pyproject.toml`).
|
|
107
|
+
- Want to rehearse first? Configure a second pending publisher on
|
|
108
|
+
<https://test.pypi.org> and add a TestPyPI step — but the production path above
|
|
109
|
+
is already `twine check`-validated.
|
|
@@ -8,7 +8,7 @@ systems. This directory is the deep-dive companion to the top-level
|
|
|
8
8
|
|
|
9
9
|
- [Benchmarks](BENCHMARKS.md) — measured detection/FPR on the bundled set **and** the public `deepset` set (regex-only vs. classifier).
|
|
10
10
|
- [Comparison](COMPARISON.md) — competitive audit + capability matrix vs. LLM Guard, LlamaFirewall, NeMo, Guardrails, Rebuff.
|
|
11
|
-
- [Releasing](RELEASING.md) —
|
|
11
|
+
- [Releasing](RELEASING.md) — PyPI Trusted Publishing plus exact scanned GHCR images and SBOMs.
|
|
12
12
|
- [Production readiness](PRODUCTION_READINESS.md) — release gates, deployment
|
|
13
13
|
checklist, known scale limits, and the next engineering milestones.
|
|
14
14
|
- [Security model](security-model.md) — threat taxonomy, layers, trust boundaries.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "shadowshield"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.2"
|
|
8
8
|
description = "Unified open-source security shield for agentic AI systems — inspired by Sentinel & ShadowClaw."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -30,6 +30,8 @@ classifiers = [
|
|
|
30
30
|
"Programming Language :: Python :: 3.10",
|
|
31
31
|
"Programming Language :: Python :: 3.11",
|
|
32
32
|
"Programming Language :: Python :: 3.12",
|
|
33
|
+
"Programming Language :: Python :: 3.13",
|
|
34
|
+
"Programming Language :: Python :: 3.14",
|
|
33
35
|
"Topic :: Security",
|
|
34
36
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
35
37
|
"Typing :: Typed",
|
|
@@ -15,6 +15,7 @@ Configuration precedence: explicit arguments first, then environment:
|
|
|
15
15
|
- ``SHADOWSHIELD_ADMIN_KEY`` - comma-separated control/metrics administrator keys.
|
|
16
16
|
- ``SHADOWSHIELD_CORS_ORIGINS`` - comma-separated list of allowed origins.
|
|
17
17
|
- ``SHADOWSHIELD_POLICY_KEY`` - HMAC key required for remotely exposed policy updates.
|
|
18
|
+
- ``SHADOWSHIELD_POLICY_STATE_KEY`` - independent HMAC key for durable policy state.
|
|
18
19
|
- ``SHADOWSHIELD_POLICY_STATE_PATH`` - durable anti-replay state file.
|
|
19
20
|
|
|
20
21
|
Keys are compared with :func:`hmac.compare_digest` (constant-time) at request time.
|
|
@@ -26,6 +27,7 @@ import asyncio
|
|
|
26
27
|
import hmac
|
|
27
28
|
import json
|
|
28
29
|
import os
|
|
30
|
+
from collections.abc import Sequence
|
|
29
31
|
from typing import Any
|
|
30
32
|
|
|
31
33
|
MAX_HTTP_BODY_BYTES = 1_048_576
|
|
@@ -75,11 +77,40 @@ def resolve_policy_key(explicit: bytes | str | None = None) -> bytes | None:
|
|
|
75
77
|
return value
|
|
76
78
|
|
|
77
79
|
|
|
80
|
+
def resolve_policy_state_key(explicit: bytes | str | None = None) -> bytes | None:
|
|
81
|
+
"""Policy-state HMAC key from args or ``SHADOWSHIELD_POLICY_STATE_KEY``."""
|
|
82
|
+
value: bytes | str | None = explicit or os.environ.get("SHADOWSHIELD_POLICY_STATE_KEY")
|
|
83
|
+
if isinstance(value, str):
|
|
84
|
+
return value.encode("utf-8") if value else None
|
|
85
|
+
return value
|
|
86
|
+
|
|
87
|
+
|
|
78
88
|
def resolve_policy_state_path(explicit: str | None = None) -> str | None:
|
|
79
89
|
"""Durable anti-replay state path from args or the deployment environment."""
|
|
80
90
|
return explicit or os.environ.get("SHADOWSHIELD_POLICY_STATE_PATH") or None
|
|
81
91
|
|
|
82
92
|
|
|
93
|
+
def secret_groups_overlap(
|
|
94
|
+
left: Sequence[bytes | str],
|
|
95
|
+
right: Sequence[bytes | str],
|
|
96
|
+
) -> bool:
|
|
97
|
+
"""Return whether two credential groups share a value.
|
|
98
|
+
|
|
99
|
+
Every candidate comparison uses :func:`hmac.compare_digest`; the loops do
|
|
100
|
+
not short-circuit when a match is found.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
overlap = False
|
|
104
|
+
for left_value in left:
|
|
105
|
+
left_bytes = left_value.encode("utf-8") if isinstance(left_value, str) else left_value
|
|
106
|
+
for right_value in right:
|
|
107
|
+
right_bytes = (
|
|
108
|
+
right_value.encode("utf-8") if isinstance(right_value, str) else right_value
|
|
109
|
+
)
|
|
110
|
+
overlap |= hmac.compare_digest(left_bytes, right_bytes)
|
|
111
|
+
return overlap
|
|
112
|
+
|
|
113
|
+
|
|
83
114
|
def extract_key(x_api_key: str | None, authorization: str | None) -> str | None:
|
|
84
115
|
"""Pull a presented key from either the ``X-API-Key`` or ``Bearer`` header."""
|
|
85
116
|
if x_api_key:
|