security-knowledge-os 0.2.0__tar.gz → 0.3.0__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.
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/PKG-INFO +1 -1
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/PUBLICATION_MANIFEST.md +21 -6
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/__init__.py +1 -1
- security_knowledge_os-0.3.0/app/adapters/capgraph.py +219 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/adapters/mcp_config.py +100 -23
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/cli.py +110 -24
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/pack-schema.md +28 -2
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/pyproject.toml +1 -1
- security_knowledge_os-0.3.0/tests/unit/test_capgraph.py +380 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_mcp_scan.py +5 -2
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/.env.example +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/.github/workflows/ci.yml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/.github/workflows/publish.yml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/.gitignore +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/LICENSE +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/NOTICE +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/README.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/REVIEW_CHECKLIST.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/REVIEW_PACKAGE.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/adapters/__init__.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/config.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/eval/__init__.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/eval/metrics.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/ingestion/__init__.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/ingestion/chunker.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/ingestion/loader.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/ingestion/parser.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/ingestion/snapshot.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/ingestion/validator.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/llm/__init__.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/llm/anthropic_client.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/llm/base.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/llm/factory.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/llm/mock.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/main.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/__init__.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/_credential_shapes.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/answer.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/assessment.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/context.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/knowledge.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/llm_io.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/pack.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/policy_outcome.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/report.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/retrieval.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/reviewer_output.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/risk.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/models/rule_clause.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/pack_cli.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/__init__.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/archive.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/build.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/diff.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/license.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/loader.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/manifest.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/signing.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/store.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/trusted_keys.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/packs/verify.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/policy/__init__.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/policy/classification.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/policy/human_gate.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/policy/knowledge_guard.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/policy/safe_test.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/retrieval/__init__.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/retrieval/base.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/retrieval/bm25.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/retrieval/hybrid.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/retrieval/index.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/__init__.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/answers.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/assess.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/attack_surface.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/clause_eval.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/evidence.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/extensions.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/facts.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/llm_review.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/normalize.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/questions.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/report.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/rollup.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/rule_engine.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/rule_loader.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/reviewer/vocabulary.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/safe_errors.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/storage/__init__.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/storage/db.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/storage/integrity.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/app/storage/repository.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/constraints.txt +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/acceptance-criteria.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/architecture.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/attribution.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/history-rewrite-map.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/knowledge-corpus.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/knowledge-schema.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/rule-schema.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/safe-test-schema.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/safety-boundaries.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/docs/threat-model.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/private/README.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/private/confidential/.gitkeep +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/private/internal/.gitkeep +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/agent-security/.gitkeep +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/agent-security/KU-0004-excessive-agency-least-privilege.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/agent-security/KU-0005-human-approval-gates.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/agent-security/KU-0006-outbound-channels-allowlist.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/credential-security/.gitkeep +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/credential-security/KU-0008-secrets-not-reachable-by-model.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/credential-security/KU-0009-credential-broker-pattern.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/governance/KU-0012-human-oversight.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/incidents/.gitkeep +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/incidents/KU-0014-llm-payload-identifier-leak.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/memory-security/.gitkeep +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/memory-security/KU-0007-persistent-memory-poisoning.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/methodology/.gitkeep +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/methodology/KU-0011-improper-output-handling.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/methodology/KU-0013-assume-compromise-ja.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/prompt-security/.gitkeep +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/prompt-security/KU-0001-direct-prompt-injection.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/prompt-security/KU-0010-system-prompt-leakage.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/rag-security/.gitkeep +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/rag-security/KU-0002-indirect-prompt-injection.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/knowledge/public/rag-security/KU-0003-untrusted-content-isolation.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/agent/.gitkeep +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/agent/OUT-001.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/agent/TOOL-000.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/agent/TOOL-001.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/credential/.gitkeep +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/credential/CRED-001.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/governance/.gitkeep +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/governance/GOV-001.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/memory/.gitkeep +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/memory/MEM-001.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/prompt/.gitkeep +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/rag/.gitkeep +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/rules/rag/PI-003.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/safe_tests/ST-CRED-001.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/safe_tests/ST-IPI-001.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/safe_tests/ST-MEM-001.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/safe_tests/ST-TOOL-001.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/sbom.json +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/_scheduled_cross_review.sh +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/assess.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/build_index.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/evaluate.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/generate_sbom.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/ingest.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/issue_license.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/preflight.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/run_cross_review.sh +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/secret_scan.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/validate_knowledge.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/validate_rules.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/scripts/validate_safe_tests.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/conftest.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/safe/S-001-prompt-only.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/safe/S-002-rag-trusted-no-actions.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/safe/S-003-readonly-tool-with-approval.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/safe/S-004-credential-proxy.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/unknown/U-001-tool-permissions-missing.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/unknown/U-002-memory-persistence-unspecified.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/unknown/U-003-outbound-destination-unspecified.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/unknown/U-004-credential-handling-unspecified.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/unknown/U-005-tool-permission-unrecognized.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/vulnerable/V-001-indirect-injection-auto-email.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/vulnerable/V-002-rag-delete-tool-no-approval.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/vulnerable/V-003-persistent-memory-untrusted.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/vulnerable/V-004-env-secret-readable.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/assessments/vulnerable/V-005-unlisted-rag-source-untrusted-memory.yaml +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/corpus/private/confidential/KU-1020-confidential-tool-matrix.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/corpus/private/internal/KU-1010-internal-memory-heuristic.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/corpus/public/credential-security/KU-1003-env-secret-exposure.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/corpus/public/prompt-security/KU-1001-instruction-data-boundary.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/corpus/public/rag-security/KU-1002-indirect-injection-external-document.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/corpus/secret/KU-1099-secret-must-not-index.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/corpus_alt/public/methodology/KU-2001-alt-corpus-marker.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/knowledge/invalid/KU-0010-mislabeled.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/knowledge/invalid/bad-classification.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/knowledge/invalid/bad-id.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/knowledge/invalid/missing-fields.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/knowledge/invalid/no-front-matter.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/knowledge/private/internal/KU-0002-internal-note.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/knowledge/public/methodology/KU-0003-sparse.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/knowledge/public/prompt-security/KU-0001-instruction-hierarchy.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/fixtures/knowledge/secret/KU-0009-secret.md +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/integration/.gitkeep +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_answers.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_anthropic_client.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_api.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_assess.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_assess_m4.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_assess_m5.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_assess_script.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_assessment_input.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_build_index_script.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_classification_gate.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_classification_leakage.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_clause_eval.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_cli.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_config.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_cross_review_script.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_db.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_eval.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_evaluate_script.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_evidence.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_fail_closed.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_finding_boundary.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_generate_sbom.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_human_gate.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_ingest_script.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_ja_retrieval.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_knowledge_corpus.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_knowledge_guard.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_knowledge_root_config.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_knowledge_schema.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_llm_adapter.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_llm_boundary.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_llm_output_schema.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_llm_review.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_loader.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_no_code_execution.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_no_secret_echo.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_normalize.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_packs.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_packs_cli.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_parser.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_policy_outcome.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_preflight_manifest.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_questions.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_reindex.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_report.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_repository.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_retrieval.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_rollup.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_rule_engine.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_rule_loader.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_safe_test.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_secret_scan.py +0 -0
- {security_knowledge_os-0.2.0 → security_knowledge_os-0.3.0}/tests/unit/test_snapshot.py +0 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
# Publication Manifest — Security Knowledge OS v0.
|
|
1
|
+
# Publication Manifest — Security Knowledge OS v0.3.0
|
|
2
2
|
|
|
3
3
|
Snapshot of what the first public push contains. Generated for the pre-publication
|
|
4
4
|
review; regenerate with `git ls-files` after any change.
|
|
5
5
|
|
|
6
|
-
- Commit: `
|
|
6
|
+
- Commit: `2627b44` (v0.3.0: whole-agent capability labels for the `capgraph` pack; 0.2.0 was `316e8c6`, 0.1.0 `a7f463d`)
|
|
7
7
|
- History: rewritten once on 2026-09-20, before first publication, to normalise author/committer identity to the
|
|
8
8
|
GitHub no-reply address (one commit message also scrubbed of a local account name). Every commit's tree is
|
|
9
9
|
identical before and after, so the reviewed content is the published content; commit hashes cited in the
|
|
10
10
|
review records below are the **pre-rewrite** ones - see `docs/history-rewrite-map.md` for old -> new.
|
|
11
11
|
- Licence: **Apache-2.0** (`LICENSE`, `NOTICE`, `pyproject.toml`)
|
|
12
|
-
- Tracked files: **
|
|
13
|
-
- Tests: **
|
|
12
|
+
- Tracked files: **244**
|
|
13
|
+
- Tests: **978** items (977 pass, 1 skip = JA-R02)
|
|
14
14
|
- `scripts/preflight.py`: **PASS** (now also verifies the documented
|
|
15
15
|
quickstart `pip install -e` command installs every declared extra,
|
|
16
16
|
pins `constraints.txt`, AND passes `--build-constraint constraints.txt`
|
|
@@ -798,6 +798,21 @@ review; regenerate with `git ls-files` after any change.
|
|
|
798
798
|
empty value in a URL, or a flag with no value, is reported as a literal
|
|
799
799
|
secret - a false positive).
|
|
800
800
|
|
|
801
|
+
**v0.3.0 capability-label review** (2026-09-29, scoped to `master..2627b44`:
|
|
802
|
+
`app/adapters/capgraph.py`, the `skos scan mcp --client` / `_agent.yaml` /
|
|
803
|
+
`capability-labels.json` changes in `app/adapters/mcp_config.py` and
|
|
804
|
+
`app/cli.py`, plus the separate data-only `capgraph` pack source). Codex,
|
|
805
|
+
three rounds, each CHANGES-REQUIRED and fixed: round 1 (`5b6a115`) 10
|
|
806
|
+
findings (4 HIGH: package impersonation via launcher path or environment,
|
|
807
|
+
server identity bound to file names, browser and memory profiles claiming
|
|
808
|
+
absent capabilities); round 2 (`75f146f`) residuals plus 2 new (HIGH: a pack
|
|
809
|
+
failing verification treated as not installed); round 3 (`2cc494b`) one
|
|
810
|
+
residual HIGH (`pnpm exec`/`yarn exec` run a local command) and one LOW
|
|
811
|
+
(npx server arguments). Fixed in `970a1b1`, reviewed by Antigravity:
|
|
812
|
+
**PASS-with-nits**; the test nit was added in `2627b44`. The medium-rule
|
|
813
|
+
suppression Codex flagged as G10 is documented in the pack README (engine
|
|
814
|
+
policy), not changed.
|
|
815
|
+
|
|
801
816
|
## Tracked files by area
|
|
802
817
|
|
|
803
818
|
Per-area counts below were last verified at round 2 (commit `bcd9519`) and
|
|
@@ -807,8 +822,8 @@ check enforces (total tracked files, total tests) are kept current above.
|
|
|
807
822
|
|
|
808
823
|
| area | files | notes |
|
|
809
824
|
| --- | --- | --- |
|
|
810
|
-
| `app/` |
|
|
811
|
-
| `tests/` |
|
|
825
|
+
| `app/` | 77 (.py) | engine, models, policy, retrieval, reviewer, llm, storage, eval, cli, main, packs (Update Pack manager), adapters (MCP config scan, capability labels) |
|
|
826
|
+
| `tests/` | 87 | 56 test modules + fixtures |
|
|
812
827
|
| `knowledge/` | 24 | 14 public KUs + `private/` skeleton (README + 2 `.gitkeep`) + category `.gitkeep`s |
|
|
813
828
|
| `rules/` | 13 | 7 rule YAMLs + category `.gitkeep`s |
|
|
814
829
|
| `safe_tests/` | 4 | 4 vetted templates |
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Capability labels for one agent, derived from its MCP client config.
|
|
2
|
+
|
|
3
|
+
Each server gets six labels - ``untrusted_input``, ``sensitive_read``,
|
|
4
|
+
``egress``, ``exec``, ``write``, ``persistence`` - valued ``True`` /
|
|
5
|
+
``False`` / ``None`` (unknown). A label is ``True`` only when the config
|
|
6
|
+
(package identity, launch options) establishes it, and ``False`` only when
|
|
7
|
+
the absence is established the same way; a server the scanner does not know
|
|
8
|
+
keeps every label ``None``. Tool descriptions are never used: they can lie.
|
|
9
|
+
|
|
10
|
+
The agent is the union of its servers and the client's own built-in tools,
|
|
11
|
+
combined with a three-valued OR (any ``True`` -> ``True``; all ``False`` ->
|
|
12
|
+
``False``; otherwise ``None``). When the client is not declared, nothing is
|
|
13
|
+
known about its built-in tools, so an aggregate ``False`` becomes ``None``.
|
|
14
|
+
|
|
15
|
+
The result feeds the ``capgraph`` Update Pack (``extensions.capgraph``) and
|
|
16
|
+
is also written as ``capability-labels.json`` for runtime enforcement (for
|
|
17
|
+
example a gateway that decides trust from the same labels). Only server
|
|
18
|
+
names from the config appear in either output (project paths are replaced by
|
|
19
|
+
opaque ids), never configuration values.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
|
|
27
|
+
from app.adapters.mcp_config import ServerScan
|
|
28
|
+
|
|
29
|
+
LABELS = ("untrusted_input", "sensitive_read", "egress", "exec", "write", "persistence")
|
|
30
|
+
# Written to extensions.capgraph; every key becomes a ``capgraph_<key>`` fact.
|
|
31
|
+
FACT_KEYS = (*LABELS, "flow_gated", "egress_gated")
|
|
32
|
+
SCHEMA = "capgraph/v0"
|
|
33
|
+
AGENT_FILE_STEM = "_agent" # reserved: no server may be written under this name
|
|
34
|
+
CLIENTS = ("claude-code", "none")
|
|
35
|
+
|
|
36
|
+
Label = bool | None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class _Caps:
|
|
41
|
+
"""What a known package can do beyond the MCP scanner's own facts."""
|
|
42
|
+
|
|
43
|
+
sensitive_read: Label = None
|
|
44
|
+
persistence: Label = None
|
|
45
|
+
write_scope: str | None = None # "fs" = the scanner's fs_scope
|
|
46
|
+
# the scanner's "no shell" does not mean "no code": a browser runs
|
|
47
|
+
# JavaScript on the pages it drives
|
|
48
|
+
exec_unknown: bool = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# By exact (ecosystem, package) identity, like the scanner's own profiles.
|
|
52
|
+
_KNOWN: dict[tuple[str, str], _Caps] = {
|
|
53
|
+
# scope-dependent: see _labels()
|
|
54
|
+
("npm", "@modelcontextprotocol/server-filesystem"): _Caps(None, True, "fs"),
|
|
55
|
+
# private repositories/messages behind the token; pushes can change CI
|
|
56
|
+
("npm", "@modelcontextprotocol/server-github"): _Caps(True, True, "remote"),
|
|
57
|
+
("npm", "@modelcontextprotocol/server-gitlab"): _Caps(True, True, "remote"),
|
|
58
|
+
("npm", "@modelcontextprotocol/server-slack"): _Caps(True, None, "remote"),
|
|
59
|
+
# a browser reaches intranet pages and the host's services, submits forms
|
|
60
|
+
# and can store content that is loaded again later (Codex capgraph G03)
|
|
61
|
+
("npm", "@modelcontextprotocol/server-puppeteer"): _Caps(None, None, "remote", True),
|
|
62
|
+
("npm", "@playwright/mcp"): _Caps(None, None, "remote", True),
|
|
63
|
+
("npm", "@modelcontextprotocol/server-brave-search"): _Caps(False, False, "none"),
|
|
64
|
+
# what it stores is loaded into later sessions; what it holds - and where
|
|
65
|
+
# the file is - is not visible (Codex capgraph G04)
|
|
66
|
+
("npm", "@modelcontextprotocol/server-memory"): _Caps(None, True, None),
|
|
67
|
+
("npm", "@wonderwhy-er/desktop-commander"): _Caps(True, True, "root"),
|
|
68
|
+
# fetch can reach localhost and intranet URLs
|
|
69
|
+
("pypi", "mcp-server-fetch"): _Caps(None, False, "none"),
|
|
70
|
+
("pypi", "mcp-server-git"): _Caps(None, None, "project"),
|
|
71
|
+
("pypi", "mcp-server-time"): _Caps(False, False, "none"),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Built-in tools of an agent client, by --client name. "none" declares a
|
|
75
|
+
# client without built-in tools (e.g. a custom agent that only uses MCP).
|
|
76
|
+
_CLIENTS: dict[str, dict[str, Label]] = {
|
|
77
|
+
# Bash, Read/Edit/Write, WebFetch/WebSearch
|
|
78
|
+
"claude-code": {
|
|
79
|
+
"untrusted_input": True, "sensitive_read": True, "egress": True,
|
|
80
|
+
"exec": True, "write": True, "persistence": True,
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class ServerLabels:
|
|
87
|
+
name: str # the scan's file stem (used in notes and file names)
|
|
88
|
+
server: str # exact server name in the config
|
|
89
|
+
scope: str # "global" or "project-<id>"
|
|
90
|
+
labels: dict[str, Label]
|
|
91
|
+
write_scope: str | None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class AgentLabels:
|
|
96
|
+
client: str | None
|
|
97
|
+
servers: list[ServerLabels]
|
|
98
|
+
labels: dict[str, Label] = field(default_factory=dict)
|
|
99
|
+
contributors: dict[str, list[str]] = field(default_factory=dict)
|
|
100
|
+
|
|
101
|
+
def facts(self) -> dict[str, Label]:
|
|
102
|
+
"""The ``extensions.capgraph`` block. The gates (``flow_gated``,
|
|
103
|
+
``egress_gated``) are not visible in a config: they are left for the
|
|
104
|
+
operator and asked when a combination they guard applies."""
|
|
105
|
+
return {**self.labels, "flow_gated": None, "egress_gated": None}
|
|
106
|
+
|
|
107
|
+
def notes(self) -> list[str]:
|
|
108
|
+
out = []
|
|
109
|
+
if self.client is None:
|
|
110
|
+
out.append(
|
|
111
|
+
"client not declared (--client): its built-in tools are unknown, so no label "
|
|
112
|
+
"is claimed absent"
|
|
113
|
+
)
|
|
114
|
+
for label in LABELS:
|
|
115
|
+
if self.contributors.get(label):
|
|
116
|
+
out.append(f"{label} <- {', '.join(self.contributors[label])}")
|
|
117
|
+
return out
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _labels(scan: ServerScan) -> ServerLabels:
|
|
121
|
+
f = scan.facts
|
|
122
|
+
labels: dict[str, Label] = {
|
|
123
|
+
"untrusted_input": f.get("ingests_untrusted_content"),
|
|
124
|
+
"sensitive_read": None,
|
|
125
|
+
"egress": f.get("can_send"),
|
|
126
|
+
"exec": f.get("shell_exec"),
|
|
127
|
+
"write": f.get("can_write"),
|
|
128
|
+
"persistence": None,
|
|
129
|
+
}
|
|
130
|
+
write_scope: str | None = None
|
|
131
|
+
caps = None
|
|
132
|
+
if scan.package is not None and scan.ecosystem is not None:
|
|
133
|
+
caps = _KNOWN.get((scan.ecosystem, scan.package))
|
|
134
|
+
if caps is not None:
|
|
135
|
+
labels["sensitive_read"] = caps.sensitive_read
|
|
136
|
+
labels["persistence"] = caps.persistence
|
|
137
|
+
write_scope = caps.write_scope
|
|
138
|
+
if caps.exec_unknown:
|
|
139
|
+
labels["exec"] = None
|
|
140
|
+
if caps.write_scope == "fs":
|
|
141
|
+
scope = f.get("fs_scope")
|
|
142
|
+
write_scope = scope if scope in ("project", "home", "root") else None
|
|
143
|
+
# the whole home directory or / holds credentials and private files;
|
|
144
|
+
# a project directory may (.env) but that is not visible here
|
|
145
|
+
labels["sensitive_read"] = True if scope in ("home", "root") else None
|
|
146
|
+
if labels["exec"] is True and f.get("sandboxed") is not True:
|
|
147
|
+
# an unsandboxed shell reads and writes anything the user can
|
|
148
|
+
labels["sensitive_read"] = labels["write"] = labels["persistence"] = True
|
|
149
|
+
write_scope = write_scope or "root"
|
|
150
|
+
if labels["write"] is False and labels["exec"] is False and labels["persistence"] is None:
|
|
151
|
+
labels["persistence"] = False
|
|
152
|
+
return ServerLabels(scan.name, scan.server, scan.scope, labels, write_scope)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _or(values: list[Label]) -> Label:
|
|
156
|
+
if any(v is True for v in values):
|
|
157
|
+
return True
|
|
158
|
+
if values and all(v is False for v in values):
|
|
159
|
+
return False
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def agent_labels(scans: list[ServerScan], client: str | None) -> AgentLabels:
|
|
164
|
+
if client is not None and client not in CLIENTS:
|
|
165
|
+
raise ValueError(f"unknown client {client!r} (known: {', '.join(CLIENTS)})")
|
|
166
|
+
servers = [_labels(s) for s in scans]
|
|
167
|
+
builtin = _CLIENTS.get(client) if client else None
|
|
168
|
+
agent = AgentLabels(client, servers)
|
|
169
|
+
for label in LABELS:
|
|
170
|
+
values = [s.labels[label] for s in servers]
|
|
171
|
+
sources = [s.name for s in servers if s.labels[label] is True]
|
|
172
|
+
if builtin is not None:
|
|
173
|
+
values.append(builtin[label])
|
|
174
|
+
if builtin[label] is True:
|
|
175
|
+
sources.insert(0, f"client:{client}")
|
|
176
|
+
value = _or(values)
|
|
177
|
+
if value is False and client is None:
|
|
178
|
+
value = None
|
|
179
|
+
agent.labels[label] = value
|
|
180
|
+
agent.contributors[label] = sources
|
|
181
|
+
return agent
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def labels_json(agent: AgentLabels, source_name: str) -> str:
|
|
185
|
+
doc = {
|
|
186
|
+
"schema": SCHEMA,
|
|
187
|
+
"source": source_name,
|
|
188
|
+
"client": agent.client,
|
|
189
|
+
"agent": agent.labels,
|
|
190
|
+
"contributors": agent.contributors,
|
|
191
|
+
# identify a server by (scope, exact name) - the file stem is only a
|
|
192
|
+
# file name and may be renamed to stay unique (Codex capgraph G02)
|
|
193
|
+
"servers": [
|
|
194
|
+
{
|
|
195
|
+
"server": s.server, "scope": s.scope, "file": s.name,
|
|
196
|
+
"labels": s.labels, "write_scope": s.write_scope,
|
|
197
|
+
}
|
|
198
|
+
for s in agent.servers
|
|
199
|
+
],
|
|
200
|
+
}
|
|
201
|
+
return json.dumps(doc, indent=2, ensure_ascii=False) + "\n"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def agent_yaml(agent: AgentLabels, source_name: str) -> str:
|
|
205
|
+
import yaml
|
|
206
|
+
|
|
207
|
+
body = yaml.safe_dump(
|
|
208
|
+
{"name": f"agent:{source_name}", "extensions": {"capgraph": agent.facts()}},
|
|
209
|
+
sort_keys=False,
|
|
210
|
+
allow_unicode=True,
|
|
211
|
+
)
|
|
212
|
+
header = [
|
|
213
|
+
f"# generated by `skos scan mcp` from {source_name} (the whole agent: every server"
|
|
214
|
+
+ (f" + client {agent.client}" if agent.client else "") + ")",
|
|
215
|
+
"# null = not established by the config. Answer what you can, then `skos assess` this"
|
|
216
|
+
" file.",
|
|
217
|
+
]
|
|
218
|
+
header += [f"# note: {n}" for n in agent.notes()]
|
|
219
|
+
return "\n".join(header) + "\n" + body
|
|
@@ -19,6 +19,7 @@ names - is ever written, printed, or placed in an error message.
|
|
|
19
19
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
|
+
import hashlib
|
|
22
23
|
import ipaddress
|
|
23
24
|
import json
|
|
24
25
|
import posixpath
|
|
@@ -86,10 +87,12 @@ _KNOWN: dict[tuple[str, str], _Profile] = {
|
|
|
86
87
|
("npm", "@modelcontextprotocol/server-github"): _Profile(False, True, True, True),
|
|
87
88
|
("npm", "@modelcontextprotocol/server-gitlab"): _Profile(False, True, True, True),
|
|
88
89
|
("npm", "@modelcontextprotocol/server-slack"): _Profile(False, True, True, True),
|
|
89
|
-
|
|
90
|
+
# a browser can submit forms and change state on the sites it visits
|
|
91
|
+
("npm", "@modelcontextprotocol/server-puppeteer"): _Profile(False, True, True, True),
|
|
90
92
|
("npm", "@modelcontextprotocol/server-brave-search"): _Profile(False, False, True, True),
|
|
91
|
-
|
|
92
|
-
("npm", "@
|
|
93
|
+
# what is stored may itself have come from outside content
|
|
94
|
+
("npm", "@modelcontextprotocol/server-memory"): _Profile(False, True, False, None),
|
|
95
|
+
("npm", "@playwright/mcp"): _Profile(False, True, True, True),
|
|
93
96
|
("npm", "@wonderwhy-er/desktop-commander"): _Profile(True, True, True, None, True),
|
|
94
97
|
("pypi", "mcp-server-fetch"): _Profile(False, False, True, True),
|
|
95
98
|
("pypi", "mcp-server-git"): _Profile(False, True, False, None, True),
|
|
@@ -129,10 +132,13 @@ class ConfigError(ValueError):
|
|
|
129
132
|
|
|
130
133
|
@dataclass
|
|
131
134
|
class ServerScan:
|
|
132
|
-
name: str
|
|
135
|
+
name: str # file-safe, unique stem for this scan's outputs
|
|
133
136
|
package: str | None
|
|
134
137
|
facts: dict[str, Any] = field(default_factory=dict)
|
|
135
138
|
notes: list[str] = field(default_factory=list)
|
|
139
|
+
ecosystem: str | None = None # "npm" / "pypi" when the package is identified
|
|
140
|
+
server: str = "" # the server's exact name in the config
|
|
141
|
+
scope: str = "global" # "global" or "project-<id>" (an opaque id, never the path)
|
|
136
142
|
|
|
137
143
|
def unknown(self) -> list[str]:
|
|
138
144
|
return [k for k in FACT_KEYS if self.facts.get(k) is None]
|
|
@@ -226,7 +232,18 @@ def _launcher(command: str, args: list[str]) -> tuple[str, str | None, list[str]
|
|
|
226
232
|
ecosystem: "npm" / "pypi" / "container" / "" (a plain command)."""
|
|
227
233
|
base = PurePosixPath(command).name
|
|
228
234
|
rest = list(args)
|
|
229
|
-
|
|
235
|
+
# `pnpm exec` / `yarn exec` run a LOCAL command: the name says nothing
|
|
236
|
+
# about a registry package (Codex capgraph review G01). `dlx` fetches one.
|
|
237
|
+
if base in ("pnpm", "yarn") and rest[:1] == ["dlx"]:
|
|
238
|
+
base, rest = "npx", rest[1:]
|
|
239
|
+
elif base in ("pnpm", "yarn") and rest[:1] == ["exec"]:
|
|
240
|
+
return "npm", None, []
|
|
241
|
+
# npm itself keeps parsing its options after the package unless they
|
|
242
|
+
# follow `--` (a trailing --registry=... replaces what runs); npx inserts
|
|
243
|
+
# that `--` before the first positional argument, so for npx the
|
|
244
|
+
# arguments after the package belong to the server (Codex G01/G13).
|
|
245
|
+
npm_exec = base == "npm" and rest[:1] == ["exec"]
|
|
246
|
+
if npm_exec:
|
|
230
247
|
base, rest = "npx", rest[1:]
|
|
231
248
|
# Only the launcher options below are understood. Anything else - a
|
|
232
249
|
# registry/index override, `-p/--from` naming a different package than
|
|
@@ -238,7 +255,13 @@ def _launcher(command: str, args: list[str]) -> tuple[str, str | None, list[str]
|
|
|
238
255
|
continue
|
|
239
256
|
if arg.startswith("-"):
|
|
240
257
|
return "npm", None, []
|
|
241
|
-
|
|
258
|
+
after = rest[i + 1 :]
|
|
259
|
+
if not npm_exec:
|
|
260
|
+
return "npm", arg, after
|
|
261
|
+
sep = after.index("--") if "--" in after else len(after)
|
|
262
|
+
if any(a.startswith("-") for a in after[:sep]):
|
|
263
|
+
return "npm", None, []
|
|
264
|
+
return "npm", arg, after[:sep] + after[sep + 1 :]
|
|
242
265
|
return "npm", None, []
|
|
243
266
|
if base in ("uvx", "pipx"):
|
|
244
267
|
if base == "pipx":
|
|
@@ -384,13 +407,33 @@ def _fs_scope(paths: list[str]) -> str | None:
|
|
|
384
407
|
return None # pragma: no cover
|
|
385
408
|
|
|
386
409
|
|
|
387
|
-
def
|
|
410
|
+
def _launch_uncertain(command: str, env: dict[str, Any]) -> str | None:
|
|
411
|
+
"""Why the configured launcher may not run the package it names, or None.
|
|
412
|
+
|
|
413
|
+
A launcher given by path is whatever that file is, and environment
|
|
414
|
+
variables other than credentials (NODE_OPTIONS, npm_config_registry,
|
|
415
|
+
PYTHONPATH, LD_PRELOAD, ...) can change what actually runs, so the
|
|
416
|
+
package is then not identified and no capability is inferred from it
|
|
417
|
+
(Codex capgraph review G01)."""
|
|
418
|
+
if command != PurePosixPath(command).name or "\\" in command:
|
|
419
|
+
return "the launcher is given by path; the package is not identified"
|
|
420
|
+
if any(re.search(_SECRET_NAME, k) is None for k in env):
|
|
421
|
+
return (
|
|
422
|
+
"environment variables other than credentials can change what runs; "
|
|
423
|
+
"the package is not identified"
|
|
424
|
+
)
|
|
425
|
+
return None
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def scan_server(label: str, cfg: object, taken: set[str], *, server: str | None = None,
|
|
429
|
+
scope: str = "global") -> ServerScan:
|
|
388
430
|
name = _safe_name(label, taken)
|
|
389
431
|
if not isinstance(cfg, dict):
|
|
390
432
|
raise ConfigError(f"server {name!r}: entry must be an object")
|
|
391
433
|
facts: dict[str, Any] = {k: None for k in FACT_KEYS}
|
|
392
434
|
notes: list[str] = []
|
|
393
435
|
package: str | None = None
|
|
436
|
+
identified: str | None = None
|
|
394
437
|
secret_seen = False
|
|
395
438
|
token_seen = False
|
|
396
439
|
url = cfg.get("url")
|
|
@@ -408,11 +451,11 @@ def scan_server(label: str, cfg: object, taken: set[str]) -> ServerScan:
|
|
|
408
451
|
except ValueError:
|
|
409
452
|
# The exception text can quote the URL (and credentials in it).
|
|
410
453
|
raise ConfigError(f"server {name!r}: 'url' is not a valid URL") from None
|
|
411
|
-
|
|
412
|
-
if
|
|
454
|
+
reach = _host_scope(host)
|
|
455
|
+
if reach == "loopback":
|
|
413
456
|
notes.append("reached via loopback; the server's own listen address is not visible")
|
|
414
457
|
else:
|
|
415
|
-
facts["bind_scope"] =
|
|
458
|
+
facts["bind_scope"] = reach # reachable at least from there
|
|
416
459
|
if has_userinfo:
|
|
417
460
|
secret_seen = token_seen = True
|
|
418
461
|
if any(re.search(_SECRET_NAME, k) for k in query_keys):
|
|
@@ -427,18 +470,26 @@ def scan_server(label: str, cfg: object, taken: set[str]) -> ServerScan:
|
|
|
427
470
|
notes.append(
|
|
428
471
|
"the client sends credentials; whether the server REQUIRES them is not visible"
|
|
429
472
|
)
|
|
430
|
-
if url.startswith("http://") and
|
|
473
|
+
if url.startswith("http://") and reach not in (None, "loopback"):
|
|
431
474
|
notes.append("remote server over plain http (no TLS)")
|
|
432
475
|
elif isinstance(cfg.get("command"), str):
|
|
433
476
|
facts["transport"] = "stdio"
|
|
434
477
|
args = _str_list(cfg.get("args"), f"server {name!r}: 'args'")
|
|
435
|
-
|
|
478
|
+
uncertain = _launch_uncertain(
|
|
479
|
+
cfg["command"], _str_dict(cfg.get("env"), f"server {name!r}: 'env'")
|
|
480
|
+
)
|
|
481
|
+
if uncertain is None:
|
|
482
|
+
ecosystem, spec, rest = _launcher(cfg["command"], args)
|
|
483
|
+
else:
|
|
484
|
+
notes.append(uncertain)
|
|
485
|
+
ecosystem, spec, rest = "", None, args
|
|
436
486
|
if ecosystem == "container":
|
|
437
487
|
facts["sandboxed"], facts["version_pinned"], extra = _container(rest)
|
|
438
488
|
notes += extra
|
|
439
489
|
elif spec is not None:
|
|
440
490
|
package, pinned = _identity(ecosystem, spec)
|
|
441
491
|
if package is not None:
|
|
492
|
+
identified = ecosystem
|
|
442
493
|
facts["version_pinned"] = pinned
|
|
443
494
|
facts["third_party"] = not (
|
|
444
495
|
(ecosystem == "npm" and package.startswith(_FIRST_PARTY_NPM_SCOPE))
|
|
@@ -471,13 +522,25 @@ def scan_server(label: str, cfg: object, taken: set[str]) -> ServerScan:
|
|
|
471
522
|
facts["token_scope"] = None
|
|
472
523
|
if not token_seen:
|
|
473
524
|
notes.append("no credential recognised in the config (this does not prove there is none)")
|
|
474
|
-
return ServerScan(
|
|
525
|
+
return ServerScan(
|
|
526
|
+
name, package, facts, notes, identified, label if server is None else server, scope
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def project_id(project: str) -> str:
|
|
531
|
+
"""An opaque, stable id for a ``projects.<path>`` entry: outputs must not
|
|
532
|
+
carry the project path (Codex capgraph review G05). Whoever knows the path
|
|
533
|
+
can recompute it."""
|
|
534
|
+
# 128 bits: two project paths must never share an id (Codex capgraph G02)
|
|
535
|
+
return "project-" + hashlib.sha256(project.encode("utf-8")).hexdigest()[:32]
|
|
475
536
|
|
|
476
537
|
|
|
477
|
-
def load_config(path: Path) -> list[tuple[str, object]]:
|
|
478
|
-
"""``[(
|
|
479
|
-
``~/.claude.json``, every
|
|
480
|
-
|
|
538
|
+
def load_config(path: Path) -> list[tuple[str, str, object]]:
|
|
539
|
+
"""``[(scope, server name, server config)]`` from the top-level
|
|
540
|
+
``mcpServers`` (scope ``global``) and, for ``~/.claude.json``, every
|
|
541
|
+
``projects.<path>.mcpServers`` (scope ``project-<id>``). Names may repeat
|
|
542
|
+
across scopes; scan_config() gives each a unique file stem, so no server
|
|
543
|
+
is ever dropped."""
|
|
481
544
|
try:
|
|
482
545
|
with path.open("rb") as fh:
|
|
483
546
|
raw = fh.read(MAX_CONFIG_BYTES + 1)
|
|
@@ -491,25 +554,39 @@ def load_config(path: Path) -> list[tuple[str, object]]:
|
|
|
491
554
|
raise ConfigError("config is not valid UTF-8 JSON") from None
|
|
492
555
|
if not isinstance(data, dict):
|
|
493
556
|
raise ConfigError("config must be a JSON object")
|
|
494
|
-
servers: list[tuple[str, object]] = []
|
|
557
|
+
servers: list[tuple[str, str, object]] = []
|
|
495
558
|
top = data.get("mcpServers")
|
|
496
559
|
if isinstance(top, dict):
|
|
497
|
-
servers += [(str(k), v) for k, v in top.items()]
|
|
560
|
+
servers += [("global", str(k), v) for k, v in top.items()]
|
|
498
561
|
projects = data.get("projects")
|
|
499
562
|
if isinstance(projects, dict):
|
|
500
563
|
for project, pdata in projects.items():
|
|
501
564
|
inner = pdata.get("mcpServers") if isinstance(pdata, dict) else None
|
|
502
565
|
if isinstance(inner, dict):
|
|
503
|
-
|
|
504
|
-
servers += [(
|
|
566
|
+
pid = project_id(str(project))
|
|
567
|
+
servers += [(pid, str(k), v) for k, v in inner.items()]
|
|
505
568
|
if not servers:
|
|
506
569
|
raise ConfigError("no MCP servers found (expected 'mcpServers')")
|
|
570
|
+
identities = [(scope, name) for scope, name, _ in servers]
|
|
571
|
+
if len(set(identities)) != len(identities):
|
|
572
|
+
# (scope, name) is how outputs identify a server; never let two share it
|
|
573
|
+
raise ConfigError("two servers share the same scope and name")
|
|
507
574
|
return servers
|
|
508
575
|
|
|
509
576
|
|
|
577
|
+
# File stems the CLI writes next to the per-server inputs.
|
|
578
|
+
RESERVED_NAMES = frozenset({"_agent"})
|
|
579
|
+
|
|
580
|
+
|
|
510
581
|
def scan_config(path: Path) -> list[ServerScan]:
|
|
511
|
-
taken: set[str] = set()
|
|
512
|
-
return [
|
|
582
|
+
taken: set[str] = set(RESERVED_NAMES)
|
|
583
|
+
return [
|
|
584
|
+
scan_server(
|
|
585
|
+
name if scope == "global" else f"{scope}.{name}", cfg, taken,
|
|
586
|
+
server=name, scope=scope,
|
|
587
|
+
)
|
|
588
|
+
for scope, name, cfg in load_config(path)
|
|
589
|
+
]
|
|
513
590
|
|
|
514
591
|
|
|
515
592
|
def assessment_yaml(scan: ServerScan, source_name: str) -> str:
|