@ahmed-g-gad/apothem 0.1.1
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.
- package/CHANGELOG.md +60 -0
- package/LICENSE +21 -0
- package/LICENSES/MIT.txt +18 -0
- package/LICENSES/PSF-2.0.txt +47 -0
- package/README.md +549 -0
- package/bin/README.md +37 -0
- package/bin/apothem.mjs +78 -0
- package/package.json +75 -0
- package/pyproject.toml +347 -0
- package/src/apothem/README.md +52 -0
- package/src/apothem/__init__.py +66 -0
- package/src/apothem/__main__.py +28 -0
- package/src/apothem/_vendor/.keep +0 -0
- package/src/apothem/_vendor/__init__.py +25 -0
- package/src/apothem/_vendor/attr/__init__.py +104 -0
- package/src/apothem/_vendor/attr/__init__.pyi +389 -0
- package/src/apothem/_vendor/attr/_cmp.py +160 -0
- package/src/apothem/_vendor/attr/_cmp.pyi +13 -0
- package/src/apothem/_vendor/attr/_compat.py +99 -0
- package/src/apothem/_vendor/attr/_config.py +31 -0
- package/src/apothem/_vendor/attr/_funcs.py +497 -0
- package/src/apothem/_vendor/attr/_make.py +3406 -0
- package/src/apothem/_vendor/attr/_next_gen.py +674 -0
- package/src/apothem/_vendor/attr/_typing_compat.pyi +15 -0
- package/src/apothem/_vendor/attr/_version_info.py +89 -0
- package/src/apothem/_vendor/attr/_version_info.pyi +9 -0
- package/src/apothem/_vendor/attr/converters.py +162 -0
- package/src/apothem/_vendor/attr/converters.pyi +19 -0
- package/src/apothem/_vendor/attr/exceptions.py +95 -0
- package/src/apothem/_vendor/attr/exceptions.pyi +17 -0
- package/src/apothem/_vendor/attr/filters.py +72 -0
- package/src/apothem/_vendor/attr/filters.pyi +6 -0
- package/src/apothem/_vendor/attr/py.typed +0 -0
- package/src/apothem/_vendor/attr/setters.py +79 -0
- package/src/apothem/_vendor/attr/setters.pyi +20 -0
- package/src/apothem/_vendor/attr/validators.py +750 -0
- package/src/apothem/_vendor/attr/validators.pyi +140 -0
- package/src/apothem/_vendor/attr.LICENSE +21 -0
- package/src/apothem/_vendor/attrs/__init__.py +72 -0
- package/src/apothem/_vendor/attrs/__init__.pyi +314 -0
- package/src/apothem/_vendor/attrs/converters.py +3 -0
- package/src/apothem/_vendor/attrs/exceptions.py +3 -0
- package/src/apothem/_vendor/attrs/filters.py +3 -0
- package/src/apothem/_vendor/attrs/py.typed +0 -0
- package/src/apothem/_vendor/attrs/setters.py +3 -0
- package/src/apothem/_vendor/attrs/validators.py +3 -0
- package/src/apothem/_vendor/attrs.LICENSE +21 -0
- package/src/apothem/_vendor/jsonschema/__init__.py +120 -0
- package/src/apothem/_vendor/jsonschema/__main__.py +6 -0
- package/src/apothem/_vendor/jsonschema/_format.py +546 -0
- package/src/apothem/_vendor/jsonschema/_keywords.py +449 -0
- package/src/apothem/_vendor/jsonschema/_legacy_keywords.py +449 -0
- package/src/apothem/_vendor/jsonschema/_types.py +204 -0
- package/src/apothem/_vendor/jsonschema/_typing.py +29 -0
- package/src/apothem/_vendor/jsonschema/_utils.py +355 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/__init__.py +5 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/const_vs_enum.py +30 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/contains.py +28 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/import_benchmark.py +31 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/issue232/issue.json +2653 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/issue232.py +25 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/json_schema_test_suite.py +12 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/nested_schemas.py +56 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/subcomponents.py +42 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/unused_registry.py +35 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/useless_applicator_schemas.py +106 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/useless_keywords.py +32 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/validator_creation.py +14 -0
- package/src/apothem/_vendor/jsonschema/cli.py +292 -0
- package/src/apothem/_vendor/jsonschema/exceptions.py +490 -0
- package/src/apothem/_vendor/jsonschema/protocols.py +230 -0
- package/src/apothem/_vendor/jsonschema/validators.py +1410 -0
- package/src/apothem/_vendor/jsonschema.LICENSE +19 -0
- package/src/apothem/_vendor/jsonschema_specifications/__init__.py +12 -0
- package/src/apothem/_vendor/jsonschema_specifications/_core.py +38 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/metaschema.json +42 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/applicator +56 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/content +17 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/core +57 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/format +14 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/meta-data +37 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/validation +98 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/metaschema.json +58 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/applicator +48 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/content +17 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/core +51 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/format-annotation +14 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/format-assertion +14 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/meta-data +37 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/unevaluated +15 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/validation +98 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft3/metaschema.json +172 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft4/metaschema.json +149 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft6/metaschema.json +153 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft7/metaschema.json +166 -0
- package/src/apothem/_vendor/jsonschema_specifications.LICENSE +19 -0
- package/src/apothem/_vendor/referencing/__init__.py +7 -0
- package/src/apothem/_vendor/referencing/_attrs.py +31 -0
- package/src/apothem/_vendor/referencing/_attrs.pyi +21 -0
- package/src/apothem/_vendor/referencing/_core.py +739 -0
- package/src/apothem/_vendor/referencing/exceptions.py +165 -0
- package/src/apothem/_vendor/referencing/jsonschema.py +642 -0
- package/src/apothem/_vendor/referencing/py.typed +0 -0
- package/src/apothem/_vendor/referencing/retrieval.py +94 -0
- package/src/apothem/_vendor/referencing/typing.py +61 -0
- package/src/apothem/_vendor/referencing.LICENSE +19 -0
- package/src/apothem/_vendor/rpds/__init__.py +251 -0
- package/src/apothem/_vendor/typing_extensions.LICENSE +279 -0
- package/src/apothem/_vendor/typing_extensions.py +4317 -0
- package/src/apothem/_vendor/vendor.txt +22 -0
- package/src/apothem/_vendor/yaml/__init__.py +389 -0
- package/src/apothem/_vendor/yaml/composer.py +138 -0
- package/src/apothem/_vendor/yaml/constructor.py +748 -0
- package/src/apothem/_vendor/yaml/cyaml.py +100 -0
- package/src/apothem/_vendor/yaml/dumper.py +61 -0
- package/src/apothem/_vendor/yaml/emitter.py +1137 -0
- package/src/apothem/_vendor/yaml/error.py +74 -0
- package/src/apothem/_vendor/yaml/events.py +85 -0
- package/src/apothem/_vendor/yaml/loader.py +63 -0
- package/src/apothem/_vendor/yaml/nodes.py +48 -0
- package/src/apothem/_vendor/yaml/parser.py +588 -0
- package/src/apothem/_vendor/yaml/reader.py +185 -0
- package/src/apothem/_vendor/yaml/representer.py +388 -0
- package/src/apothem/_vendor/yaml/resolver.py +226 -0
- package/src/apothem/_vendor/yaml/scanner.py +1435 -0
- package/src/apothem/_vendor/yaml/serializer.py +110 -0
- package/src/apothem/_vendor/yaml/tokens.py +103 -0
- package/src/apothem/_vendor/yaml.LICENSE +20 -0
- package/src/apothem/agents/README.md +60 -0
- package/src/apothem/agents/codebase-explorer.md +91 -0
- package/src/apothem/agents/convention-auditor.md +93 -0
- package/src/apothem/agents/dependency-auditor.md +97 -0
- package/src/apothem/agents/fact-checker.md +84 -0
- package/src/apothem/agents/mcp-builder.md +86 -0
- package/src/apothem/agents/memory-auditor.md +93 -0
- package/src/apothem/agents/prompt-evaluator.md +87 -0
- package/src/apothem/agents/quality-gate.md +103 -0
- package/src/apothem/agents/refactor-surgeon.md +74 -0
- package/src/apothem/agents/research-scout.md +73 -0
- package/src/apothem/agents/security-scanner.md +83 -0
- package/src/apothem/agents/test-runner.md +84 -0
- package/src/apothem/audit/README.md +73 -0
- package/src/apothem/audit/_scan_lib.py +182 -0
- package/src/apothem/audit/analyze_graph.py +260 -0
- package/src/apothem/audit/build_capability_graph.py +607 -0
- package/src/apothem/audit/build_inventory.py +657 -0
- package/src/apothem/audit/build_plans_provenance.py +997 -0
- package/src/apothem/audit/check_links.py +389 -0
- package/src/apothem/audit/classify_artifacts.py +381 -0
- package/src/apothem/audit/deprecated-tokens.txt +10 -0
- package/src/apothem/audit/execute_plans_migration.py +491 -0
- package/src/apothem/audit/known-projects.txt +15 -0
- package/src/apothem/audit/render_capability_index.py +467 -0
- package/src/apothem/audit/render_inventory.py +405 -0
- package/src/apothem/audit/scan_ai_surfaces.py +1125 -0
- package/src/apothem/audit/scan_ai_surfaces_coarse.py +261 -0
- package/src/apothem/audit/scan_drift_features.py +143 -0
- package/src/apothem/audit/scan_frontmatter.py +293 -0
- package/src/apothem/audit/scan_header_coverage.py +1134 -0
- package/src/apothem/audit/scan_plan_leakage.py +540 -0
- package/src/apothem/audit/scan_plans_discipline.py +188 -0
- package/src/apothem/audit/scan_secrets_pii.py +245 -0
- package/src/apothem/audit/scan_stale_tokens.py +296 -0
- package/src/apothem/audit/synthesize_drift.py +205 -0
- package/src/apothem/benchmarks/README.md +33 -0
- package/src/apothem/benchmarks/__init__.py +3 -0
- package/src/apothem/benchmarks/bench_agents.py +63 -0
- package/src/apothem/benchmarks/bench_hooks.py +93 -0
- package/src/apothem/benchmarks/bench_install.py +58 -0
- package/src/apothem/benchmarks/bench_tests.py +93 -0
- package/src/apothem/benchmarks/bench_validate_ecosystem.py +84 -0
- package/src/apothem/cli/README.md +33 -0
- package/src/apothem/cli/__init__.py +229 -0
- package/src/apothem/cli/_cmd_completion.py +88 -0
- package/src/apothem/cli/_cmd_diff.py +181 -0
- package/src/apothem/cli/_cmd_doctor.py +143 -0
- package/src/apothem/cli/_cmd_harnesses.py +167 -0
- package/src/apothem/cli/_cmd_install.py +327 -0
- package/src/apothem/cli/_cmd_migrate_workspace.py +143 -0
- package/src/apothem/cli/_cmd_profile.py +341 -0
- package/src/apothem/cli/_cmd_status.py +180 -0
- package/src/apothem/cli/_cmd_uninstall.py +215 -0
- package/src/apothem/cli/_cmd_update.py +397 -0
- package/src/apothem/cli/_cmd_verify.py +194 -0
- package/src/apothem/cli/_common_flags.py +90 -0
- package/src/apothem/cli/_epilogs.py +296 -0
- package/src/apothem/cli/_helpers.py +857 -0
- package/src/apothem/cli/_json_formatter.py +21 -0
- package/src/apothem/cli/_materialize.py +376 -0
- package/src/apothem/cli/completions/apothem.bash +30 -0
- package/src/apothem/cli/completions/apothem.fish +19 -0
- package/src/apothem/cli/completions/apothem.ps1 +27 -0
- package/src/apothem/cli/completions/apothem.zsh +42 -0
- package/src/apothem/cli/reference_export.py +126 -0
- package/src/apothem/commands/README.md +125 -0
- package/src/apothem/commands/a11y-audit.md +203 -0
- package/src/apothem/commands/architecture-review.md +194 -0
- package/src/apothem/commands/audit.md +165 -0
- package/src/apothem/commands/code-audit.md +218 -0
- package/src/apothem/commands/code-review.md +193 -0
- package/src/apothem/commands/dependency-audit.md +209 -0
- package/src/apothem/commands/docs-review.md +199 -0
- package/src/apothem/commands/elevate.md +285 -0
- package/src/apothem/commands/eval.md +149 -0
- package/src/apothem/commands/fortress.md +172 -0
- package/src/apothem/commands/freshify.md +168 -0
- package/src/apothem/commands/github-deploy-fresh.md +178 -0
- package/src/apothem/commands/github-deploy-next.md +167 -0
- package/src/apothem/commands/perf-audit.md +198 -0
- package/src/apothem/commands/plan-amend.md +104 -0
- package/src/apothem/commands/plan-audit.md +127 -0
- package/src/apothem/commands/plan-design.md +257 -0
- package/src/apothem/commands/plan-execute.md +495 -0
- package/src/apothem/commands/plan-generate.md +351 -0
- package/src/apothem/commands/plan-review.md +555 -0
- package/src/apothem/commands/plan-spec.md +359 -0
- package/src/apothem/commands/plan-status.md +222 -0
- package/src/apothem/commands/plan.md +173 -0
- package/src/apothem/commands/projectify.md +142 -0
- package/src/apothem/commands/release-readiness.md +142 -0
- package/src/apothem/commands/research-analysis.md +241 -0
- package/src/apothem/commands/research-design.md +231 -0
- package/src/apothem/commands/research-disseminate.md +225 -0
- package/src/apothem/commands/research-experiment.md +232 -0
- package/src/apothem/commands/research-ideate.md +213 -0
- package/src/apothem/commands/research-paper.md +252 -0
- package/src/apothem/commands/research-proposal.md +220 -0
- package/src/apothem/commands/research-publish.md +255 -0
- package/src/apothem/commands/research-review.md +251 -0
- package/src/apothem/commands/research-sources.md +266 -0
- package/src/apothem/commands/research-spec.md +255 -0
- package/src/apothem/commands/research-synthesis.md +233 -0
- package/src/apothem/commands/research-theory.md +218 -0
- package/src/apothem/commands/research.md +181 -0
- package/src/apothem/commands/security-audit.md +196 -0
- package/src/apothem/commands/supply-chain-audit.md +192 -0
- package/src/apothem/commands/test-suite.md +146 -0
- package/src/apothem/commands/threat-model-audit.md +199 -0
- package/src/apothem/commands/ux-review.md +202 -0
- package/src/apothem/commands/workflow.md +162 -0
- package/src/apothem/conformity/README.md +173 -0
- package/src/apothem/conformity/__init__.py +1 -0
- package/src/apothem/conformity/_grep_base.py +93 -0
- package/src/apothem/conformity/agent_capability_grep.py +306 -0
- package/src/apothem/conformity/agents_md_coverage_grep.py +382 -0
- package/src/apothem/conformity/agnosticism_grep.py +311 -0
- package/src/apothem/conformity/always_on_budget_grep.py +318 -0
- package/src/apothem/conformity/bare_except_grep.py +115 -0
- package/src/apothem/conformity/binding_reciprocity_grep.py +151 -0
- package/src/apothem/conformity/brand_mark_grep.py +272 -0
- package/src/apothem/conformity/commented_out_code_grep.py +176 -0
- package/src/apothem/conformity/completion_claim_grep.py +169 -0
- package/src/apothem/conformity/conventional_commit_grep.py +319 -0
- package/src/apothem/conformity/copilot_instructions_presence_grep.py +324 -0
- package/src/apothem/conformity/cross_platform_matrix_grep.py +297 -0
- package/src/apothem/conformity/determinism_grep.py +306 -0
- package/src/apothem/conformity/diagram_staleness_grep.py +154 -0
- package/src/apothem/conformity/dynamism_grep.py +284 -0
- package/src/apothem/conformity/editorconfig_presence_grep.py +281 -0
- package/src/apothem/conformity/file_header_grep.py +502 -0
- package/src/apothem/conformity/freshness_token_grep.py +233 -0
- package/src/apothem/conformity/frontmatter_grep.py +274 -0
- package/src/apothem/conformity/frontmatter_value_grep.py +386 -0
- package/src/apothem/conformity/gate.py +1386 -0
- package/src/apothem/conformity/gitattributes_presence_grep.py +238 -0
- package/src/apothem/conformity/harden_runner_grep.py +320 -0
- package/src/apothem/conformity/hedging_grep.py +129 -0
- package/src/apothem/conformity/license_author_consistency_grep.py +204 -0
- package/src/apothem/conformity/link_check.py +327 -0
- package/src/apothem/conformity/magic_number_grep.py +182 -0
- package/src/apothem/conformity/multi_surface_coherence_grep.py +620 -0
- package/src/apothem/conformity/naming_grep.py +224 -0
- package/src/apothem/conformity/no_global_plans_grep.py +339 -0
- package/src/apothem/conformity/no_toplevel_docs_grep.py +120 -0
- package/src/apothem/conformity/oidc_trusted_publishing_grep.py +291 -0
- package/src/apothem/conformity/option_annotation_grep.py +352 -0
- package/src/apothem/conformity/orphan_output_grep.py +206 -0
- package/src/apothem/conformity/permissions_minimum_scope_grep.py +299 -0
- package/src/apothem/conformity/plain_language_grep.py +559 -0
- package/src/apothem/conformity/plan_next_step_consistency_grep.py +450 -0
- package/src/apothem/conformity/plan_suite_structure_grep.py +534 -0
- package/src/apothem/conformity/plans_discipline_language_grep.py +245 -0
- package/src/apothem/conformity/production_ready_pr_grep.py +200 -0
- package/src/apothem/conformity/recommend_next_step_grep.py +250 -0
- package/src/apothem/conformity/redundancy_grep.py +401 -0
- package/src/apothem/conformity/reference_token_grep.py +230 -0
- package/src/apothem/conformity/registry_capability_consistency_grep.py +368 -0
- package/src/apothem/conformity/secret_leak_grep.py +193 -0
- package/src/apothem/conformity/semver_stability_grep.py +358 -0
- package/src/apothem/conformity/smoke_install_grep.py +194 -0
- package/src/apothem/conformity/static_version_grep.py +284 -0
- package/src/apothem/conformity/token_efficiency_grep.py +185 -0
- package/src/apothem/conformity/unpinned_action_grep.py +115 -0
- package/src/apothem/conformity/user_confirm_grep.py +74 -0
- package/src/apothem/conformity/workflow_concurrency_grep.py +283 -0
- package/src/apothem/harnesses/README.md +63 -0
- package/src/apothem/harnesses/__init__.py +16 -0
- package/src/apothem/harnesses/_shared/README.md +36 -0
- package/src/apothem/harnesses/_shared/__init__.py +12 -0
- package/src/apothem/harnesses/_shared/install_driver.py +281 -0
- package/src/apothem/harnesses/_shared/install_driver_apply.py +612 -0
- package/src/apothem/harnesses/_shared/install_driver_backup.py +535 -0
- package/src/apothem/harnesses/_shared/install_driver_converters.py +310 -0
- package/src/apothem/harnesses/_shared/install_driver_lifecycle.py +495 -0
- package/src/apothem/harnesses/_shared/install_driver_materialize.py +675 -0
- package/src/apothem/harnesses/_shared/install_driver_merge.py +656 -0
- package/src/apothem/harnesses/_shared/install_driver_pathsafety.py +137 -0
- package/src/apothem/harnesses/_shared/install_driver_planvalidation.py +240 -0
- package/src/apothem/harnesses/_shared/install_driver_removal.py +366 -0
- package/src/apothem/harnesses/_shared/install_driver_treeops.py +248 -0
- package/src/apothem/harnesses/_shared/install_driver_types.py +330 -0
- package/src/apothem/harnesses/_shared/wrapper_factories.py +448 -0
- package/src/apothem/harnesses/antigravity/STANDARD-CONVENTION-PIN.md +91 -0
- package/src/apothem/harnesses/antigravity/__init__.py +70 -0
- package/src/apothem/harnesses/antigravity/capabilities.yml +40 -0
- package/src/apothem/harnesses/antigravity/install.py +63 -0
- package/src/apothem/harnesses/antigravity/templates/GEMINI.md +40 -0
- package/src/apothem/harnesses/antigravity/templates/plugin.json +5 -0
- package/src/apothem/harnesses/antigravity/uninstall.py +22 -0
- package/src/apothem/harnesses/antigravity/update.py +10 -0
- package/src/apothem/harnesses/antigravity/verify.py +11 -0
- package/src/apothem/harnesses/claude_code/STANDARD-CONVENTION-PIN.md +65 -0
- package/src/apothem/harnesses/claude_code/__init__.py +107 -0
- package/src/apothem/harnesses/claude_code/capabilities.yml +42 -0
- package/src/apothem/harnesses/claude_code/install.py +147 -0
- package/src/apothem/harnesses/claude_code/templates/settings.json +351 -0
- package/src/apothem/harnesses/claude_code/uninstall.py +23 -0
- package/src/apothem/harnesses/claude_code/update.py +10 -0
- package/src/apothem/harnesses/claude_code/verify.py +11 -0
- package/src/apothem/harnesses/codebuddy/STANDARD-CONVENTION-PIN.md +74 -0
- package/src/apothem/harnesses/codebuddy/__init__.py +49 -0
- package/src/apothem/harnesses/codebuddy/capabilities.yml +34 -0
- package/src/apothem/harnesses/codebuddy/install.py +40 -0
- package/src/apothem/harnesses/codebuddy/templates/apothem-rules.md +37 -0
- package/src/apothem/harnesses/codebuddy/uninstall.py +25 -0
- package/src/apothem/harnesses/codebuddy/update.py +10 -0
- package/src/apothem/harnesses/codebuddy/verify.py +11 -0
- package/src/apothem/harnesses/codex/STANDARD-CONVENTION-PIN.md +79 -0
- package/src/apothem/harnesses/codex/__init__.py +72 -0
- package/src/apothem/harnesses/codex/capabilities.yml +40 -0
- package/src/apothem/harnesses/codex/install.py +69 -0
- package/src/apothem/harnesses/codex/templates/AGENTS.md +40 -0
- package/src/apothem/harnesses/codex/templates/hooks.json +127 -0
- package/src/apothem/harnesses/codex/uninstall.py +23 -0
- package/src/apothem/harnesses/codex/update.py +10 -0
- package/src/apothem/harnesses/codex/verify.py +11 -0
- package/src/apothem/harnesses/cursor/STANDARD-CONVENTION-PIN.md +79 -0
- package/src/apothem/harnesses/cursor/__init__.py +48 -0
- package/src/apothem/harnesses/cursor/capabilities.yml +42 -0
- package/src/apothem/harnesses/cursor/install.py +38 -0
- package/src/apothem/harnesses/cursor/templates/apothem-rules.mdc +40 -0
- package/src/apothem/harnesses/cursor/uninstall.py +25 -0
- package/src/apothem/harnesses/cursor/update.py +10 -0
- package/src/apothem/harnesses/cursor/verify.py +11 -0
- package/src/apothem/harnesses/gemini_cli/STANDARD-CONVENTION-PIN.md +102 -0
- package/src/apothem/harnesses/gemini_cli/__init__.py +52 -0
- package/src/apothem/harnesses/gemini_cli/capabilities.yml +43 -0
- package/src/apothem/harnesses/gemini_cli/install.py +43 -0
- package/src/apothem/harnesses/gemini_cli/templates/GEMINI.md +38 -0
- package/src/apothem/harnesses/gemini_cli/uninstall.py +25 -0
- package/src/apothem/harnesses/gemini_cli/update.py +10 -0
- package/src/apothem/harnesses/gemini_cli/verify.py +11 -0
- package/src/apothem/harnesses/github_copilot/STANDARD-CONVENTION-PIN.md +84 -0
- package/src/apothem/harnesses/github_copilot/__init__.py +47 -0
- package/src/apothem/harnesses/github_copilot/capabilities.yml +42 -0
- package/src/apothem/harnesses/github_copilot/install.py +40 -0
- package/src/apothem/harnesses/github_copilot/templates/copilot-instructions.md +33 -0
- package/src/apothem/harnesses/github_copilot/uninstall.py +25 -0
- package/src/apothem/harnesses/github_copilot/update.py +10 -0
- package/src/apothem/harnesses/github_copilot/verify.py +11 -0
- package/src/apothem/harnesses/glm/STANDARD-CONVENTION-PIN.md +77 -0
- package/src/apothem/harnesses/glm/__init__.py +56 -0
- package/src/apothem/harnesses/glm/capabilities.yml +33 -0
- package/src/apothem/harnesses/glm/install.py +45 -0
- package/src/apothem/harnesses/glm/templates/glm.toml +58 -0
- package/src/apothem/harnesses/glm/uninstall.py +25 -0
- package/src/apothem/harnesses/glm/update.py +10 -0
- package/src/apothem/harnesses/glm/verify.py +11 -0
- package/src/apothem/harnesses/hermes/STANDARD-CONVENTION-PIN.md +57 -0
- package/src/apothem/harnesses/hermes/__init__.py +33 -0
- package/src/apothem/harnesses/hermes/capabilities.yml +36 -0
- package/src/apothem/harnesses/hermes/install.py +17 -0
- package/src/apothem/harnesses/hermes/materializer.py +35 -0
- package/src/apothem/harnesses/hermes/uninstall.py +33 -0
- package/src/apothem/harnesses/hermes/update.py +10 -0
- package/src/apothem/harnesses/hermes/verify.py +11 -0
- package/src/apothem/harnesses/kimi_code/STANDARD-CONVENTION-PIN.md +128 -0
- package/src/apothem/harnesses/kimi_code/__init__.py +59 -0
- package/src/apothem/harnesses/kimi_code/capabilities.yml +40 -0
- package/src/apothem/harnesses/kimi_code/install.py +42 -0
- package/src/apothem/harnesses/kimi_code/templates/AGENTS.md +43 -0
- package/src/apothem/harnesses/kimi_code/uninstall.py +27 -0
- package/src/apothem/harnesses/kimi_code/update.py +10 -0
- package/src/apothem/harnesses/kimi_code/verify.py +11 -0
- package/src/apothem/harnesses/kiro/STANDARD-CONVENTION-PIN.md +77 -0
- package/src/apothem/harnesses/kiro/__init__.py +49 -0
- package/src/apothem/harnesses/kiro/capabilities.yml +36 -0
- package/src/apothem/harnesses/kiro/install.py +39 -0
- package/src/apothem/harnesses/kiro/templates/apothem-rules.md +36 -0
- package/src/apothem/harnesses/kiro/uninstall.py +25 -0
- package/src/apothem/harnesses/kiro/update.py +10 -0
- package/src/apothem/harnesses/kiro/verify.py +11 -0
- package/src/apothem/harnesses/open_claw/STANDARD-CONVENTION-PIN.md +62 -0
- package/src/apothem/harnesses/open_claw/__init__.py +35 -0
- package/src/apothem/harnesses/open_claw/capabilities.yml +35 -0
- package/src/apothem/harnesses/open_claw/install.py +17 -0
- package/src/apothem/harnesses/open_claw/materializer.py +36 -0
- package/src/apothem/harnesses/open_claw/uninstall.py +32 -0
- package/src/apothem/harnesses/open_claw/update.py +10 -0
- package/src/apothem/harnesses/open_claw/verify.py +11 -0
- package/src/apothem/harnesses/opencode/STANDARD-CONVENTION-PIN.md +76 -0
- package/src/apothem/harnesses/opencode/__init__.py +35 -0
- package/src/apothem/harnesses/opencode/capabilities.yml +43 -0
- package/src/apothem/harnesses/opencode/install.py +17 -0
- package/src/apothem/harnesses/opencode/materializer.py +31 -0
- package/src/apothem/harnesses/opencode/uninstall.py +34 -0
- package/src/apothem/harnesses/opencode/update.py +10 -0
- package/src/apothem/harnesses/opencode/verify.py +11 -0
- package/src/apothem/harnesses/qwen_code/STANDARD-CONVENTION-PIN.md +87 -0
- package/src/apothem/harnesses/qwen_code/__init__.py +37 -0
- package/src/apothem/harnesses/qwen_code/capabilities.yml +43 -0
- package/src/apothem/harnesses/qwen_code/install.py +19 -0
- package/src/apothem/harnesses/qwen_code/materializer.py +174 -0
- package/src/apothem/harnesses/qwen_code/templates/QWEN.md +30 -0
- package/src/apothem/harnesses/qwen_code/uninstall.py +34 -0
- package/src/apothem/harnesses/qwen_code/update.py +10 -0
- package/src/apothem/harnesses/qwen_code/verify.py +11 -0
- package/src/apothem/harnesses/trae/STANDARD-CONVENTION-PIN.md +70 -0
- package/src/apothem/harnesses/trae/__init__.py +49 -0
- package/src/apothem/harnesses/trae/capabilities.yml +34 -0
- package/src/apothem/harnesses/trae/install.py +38 -0
- package/src/apothem/harnesses/trae/templates/apothem-rules.md +37 -0
- package/src/apothem/harnesses/trae/uninstall.py +25 -0
- package/src/apothem/harnesses/trae/update.py +10 -0
- package/src/apothem/harnesses/trae/verify.py +11 -0
- package/src/apothem/harnesses/windsurf/STANDARD-CONVENTION-PIN.md +91 -0
- package/src/apothem/harnesses/windsurf/__init__.py +52 -0
- package/src/apothem/harnesses/windsurf/capabilities.yml +40 -0
- package/src/apothem/harnesses/windsurf/install.py +41 -0
- package/src/apothem/harnesses/windsurf/templates/apothem-rules.md +37 -0
- package/src/apothem/harnesses/windsurf/uninstall.py +25 -0
- package/src/apothem/harnesses/windsurf/update.py +10 -0
- package/src/apothem/harnesses/windsurf/verify.py +11 -0
- package/src/apothem/harnesses/zed/STANDARD-CONVENTION-PIN.md +92 -0
- package/src/apothem/harnesses/zed/__init__.py +57 -0
- package/src/apothem/harnesses/zed/capabilities.yml +38 -0
- package/src/apothem/harnesses/zed/install.py +41 -0
- package/src/apothem/harnesses/zed/templates/apothem-rules.md +32 -0
- package/src/apothem/harnesses/zed/uninstall.py +28 -0
- package/src/apothem/harnesses/zed/update.py +10 -0
- package/src/apothem/harnesses/zed/verify.py +11 -0
- package/src/apothem/hooks/README.md +81 -0
- package/src/apothem/hooks/__init__.py +24 -0
- package/src/apothem/hooks/askuserquestion_validator.py +380 -0
- package/src/apothem/hooks/dispatch.py +296 -0
- package/src/apothem/hooks/emit_hook_context.py +444 -0
- package/src/apothem/hooks/hooks.json +318 -0
- package/src/apothem/hooks/lib/README.md +39 -0
- package/src/apothem/hooks/lib/__init__.py +18 -0
- package/src/apothem/hooks/lib/bootstrap.ps1 +129 -0
- package/src/apothem/hooks/lib/bootstrap.sh +103 -0
- package/src/apothem/hooks/lib/events.py +51 -0
- package/src/apothem/hooks/lib/find-pwsh.ps1 +78 -0
- package/src/apothem/hooks/lib/find-pwsh.sh +76 -0
- package/src/apothem/hooks/lib/find-python.ps1 +63 -0
- package/src/apothem/hooks/lib/find-python.sh +97 -0
- package/src/apothem/hooks/lib/log.py +43 -0
- package/src/apothem/hooks/lib/resolve_root.py +264 -0
- package/src/apothem/hooks/messages/postcompact.md +14 -0
- package/src/apothem/hooks/messages/posttooluse-proactive-compaction.md +46 -0
- package/src/apothem/hooks/messages/precompact.md +14 -0
- package/src/apothem/hooks/messages/pretooluse-askuserquestion-recommended.md +65 -0
- package/src/apothem/hooks/messages/pretooluse-bash-plan-guard.md +97 -0
- package/src/apothem/hooks/messages/pretooluse-bash.md +39 -0
- package/src/apothem/hooks/messages/pretooluse-conformity.md +70 -0
- package/src/apothem/hooks/messages/pretooluse-dependency-guard.md +21 -0
- package/src/apothem/hooks/messages/pretooluse-edit-header-guard.md +61 -0
- package/src/apothem/hooks/messages/pretooluse-edit.md +21 -0
- package/src/apothem/hooks/messages/pretooluse-eval-guard.md +39 -0
- package/src/apothem/hooks/messages/pretooluse-notebookedit.md +11 -0
- package/src/apothem/hooks/messages/pretooluse-write-header-guard.md +45 -0
- package/src/apothem/hooks/messages/pretooluse-write-plan-guard.md +72 -0
- package/src/apothem/hooks/messages/pretooluse-write.md +21 -0
- package/src/apothem/hooks/messages/sessionstart.md +15 -0
- package/src/apothem/hooks/messages/stop.md +27 -0
- package/src/apothem/hooks/proactive_compaction_tracker.py +327 -0
- package/src/apothem/hooks/session_start_bootstrap.py +472 -0
- package/src/apothem/lib/README.md +42 -0
- package/src/apothem/lib/__init__.py +13 -0
- package/src/apothem/lib/atomic_io.py +189 -0
- package/src/apothem/lib/auditor.py +687 -0
- package/src/apothem/lib/clean_slate.py +396 -0
- package/src/apothem/lib/contexts.py +352 -0
- package/src/apothem/lib/data_home.py +255 -0
- package/src/apothem/lib/frontmatter.py +101 -0
- package/src/apothem/lib/harness_materializer.py +213 -0
- package/src/apothem/lib/harness_protocol.py +59 -0
- package/src/apothem/lib/harness_registry.py +282 -0
- package/src/apothem/lib/harness_registry_data.py +843 -0
- package/src/apothem/lib/install_ledger.py +347 -0
- package/src/apothem/lib/learning.py +540 -0
- package/src/apothem/lib/memory.py +347 -0
- package/src/apothem/lib/parallel_sweep.py +234 -0
- package/src/apothem/lib/plan_tiers.py +200 -0
- package/src/apothem/lib/plugin_bootstrap.py +132 -0
- package/src/apothem/lib/plugin_tree.py +599 -0
- package/src/apothem/lib/profile.py +755 -0
- package/src/apothem/lib/profile_projection.py +198 -0
- package/src/apothem/lib/propagation-manifest.yaml +878 -0
- package/src/apothem/lib/propagation.py +220 -0
- package/src/apothem/lib/python_resolver.py +189 -0
- package/src/apothem/lib/reporter.py +62 -0
- package/src/apothem/lib/workspace_migration.py +323 -0
- package/src/apothem/output-styles/README.md +41 -0
- package/src/apothem/output-styles/concise-engineer.md +49 -0
- package/src/apothem/output-styles/default-architect.md +52 -0
- package/src/apothem/output-styles/default.md +113 -0
- package/src/apothem/output-styles/forensic-auditor.md +63 -0
- package/src/apothem/py.typed +0 -0
- package/src/apothem/rules/README.md +121 -0
- package/src/apothem/rules/agent-capability-discipline-matrix.md +89 -0
- package/src/apothem/rules/agent-capability-discipline.md +78 -0
- package/src/apothem/rules/agent-orchestration-patterns.md +144 -0
- package/src/apothem/rules/agent-orchestration.md +65 -0
- package/src/apothem/rules/agents-md-convention.md +86 -0
- package/src/apothem/rules/agile-sprints-elements.md +135 -0
- package/src/apothem/rules/agile-sprints.md +64 -0
- package/src/apothem/rules/agnostic-posture-checklist.md +47 -0
- package/src/apothem/rules/agnostic-posture.md +48 -0
- package/src/apothem/rules/authoritative-referencing-quotation.md +50 -0
- package/src/apothem/rules/authoritative-referencing.md +66 -0
- package/src/apothem/rules/authority-inquiry-categories.md +58 -0
- package/src/apothem/rules/authority-inquiry.md +54 -0
- package/src/apothem/rules/auto-memory-topic-files.md +86 -0
- package/src/apothem/rules/auto-memory.md +67 -0
- package/src/apothem/rules/bidirectional-binding.md +123 -0
- package/src/apothem/rules/canonical-layout-reporting-tiers.md +212 -0
- package/src/apothem/rules/canonical-layout.md +60 -0
- package/src/apothem/rules/clean-architecture-layers.md +186 -0
- package/src/apothem/rules/clean-room-generation-protocols.md +124 -0
- package/src/apothem/rules/clean-room-generation.md +59 -0
- package/src/apothem/rules/code-craft-conventions.md +101 -0
- package/src/apothem/rules/code-craft-markdown.md +138 -0
- package/src/apothem/rules/code-craft-python.md +154 -0
- package/src/apothem/rules/code-craft-shell.md +192 -0
- package/src/apothem/rules/cognitive-identity-techniques.md +180 -0
- package/src/apothem/rules/cognitive-identity.md +81 -0
- package/src/apothem/rules/context-management-budget.md +46 -0
- package/src/apothem/rules/context-management-protocol.md +161 -0
- package/src/apothem/rules/context-management-scratch.md +128 -0
- package/src/apothem/rules/context-management.md +85 -0
- package/src/apothem/rules/definitiveness-virtues.md +67 -0
- package/src/apothem/rules/definitiveness.md +58 -0
- package/src/apothem/rules/determinism.md +81 -0
- package/src/apothem/rules/disclosure-ledger-markers.md +58 -0
- package/src/apothem/rules/disclosure-ledger.md +52 -0
- package/src/apothem/rules/dynamism.md +38 -0
- package/src/apothem/rules/etc-extension.md +57 -0
- package/src/apothem/rules/expertise-posture-elements.md +68 -0
- package/src/apothem/rules/expertise-posture.md +54 -0
- package/src/apothem/rules/freshness-facade.md +64 -0
- package/src/apothem/rules/harness-adapter-shape-schemas.md +162 -0
- package/src/apothem/rules/harness-adapter-shape.md +42 -0
- package/src/apothem/rules/host-discovery-manifests.md +50 -0
- package/src/apothem/rules/host-discovery.md +56 -0
- package/src/apothem/rules/i18n-discipline-locale-cohorts.md +120 -0
- package/src/apothem/rules/i18n-discipline.md +70 -0
- package/src/apothem/rules/interactive-questions-canonical-shapes.md +590 -0
- package/src/apothem/rules/interactive-questions-detail.md +41 -0
- package/src/apothem/rules/interactive-questions-sweep-matchers.md +184 -0
- package/src/apothem/rules/interactive-questions.md +89 -0
- package/src/apothem/rules/large-file-generation.md +112 -0
- package/src/apothem/rules/large-file-reading.md +59 -0
- package/src/apothem/rules/living-docs.md +85 -0
- package/src/apothem/rules/multi-agent-workflow.md +57 -0
- package/src/apothem/rules/operational-mandates-expanded.md +78 -0
- package/src/apothem/rules/operational-mandates.md +88 -0
- package/src/apothem/rules/option-annotation-form.md +60 -0
- package/src/apothem/rules/option-annotation.md +45 -0
- package/src/apothem/rules/own-voice-reimplementation.md +86 -0
- package/src/apothem/rules/performance-discipline.md +91 -0
- package/src/apothem/rules/persistent-conventions-vigilance-checklist.md +54 -0
- package/src/apothem/rules/persistent-conventions-vigilance.md +61 -0
- package/src/apothem/rules/plain-language.md +56 -0
- package/src/apothem/rules/planning-techniques.md +130 -0
- package/src/apothem/rules/pre-emission-gate-bars.md +86 -0
- package/src/apothem/rules/pre-emission-gate.md +54 -0
- package/src/apothem/rules/production-ready-prs-surfaces.md +162 -0
- package/src/apothem/rules/production-ready-prs.md +83 -0
- package/src/apothem/rules/propagation.md +63 -0
- package/src/apothem/rules/recommend-next-step.md +106 -0
- package/src/apothem/rules/refactoring-discipline.md +76 -0
- package/src/apothem/rules/session-closure.md +44 -0
- package/src/apothem/rules/sota-elevation-exemplars.md +76 -0
- package/src/apothem/rules/sota-elevation.md +52 -0
- package/src/apothem/rules/source-accessibility.md +58 -0
- package/src/apothem/rules/surgical-manipulation.md +48 -0
- package/src/apothem/rules/systemic-participation-relations.md +108 -0
- package/src/apothem/rules/systemic-participation.md +70 -0
- package/src/apothem/rules/ten-dimension-check-dimensions.md +52 -0
- package/src/apothem/rules/ten-dimension-check.md +59 -0
- package/src/apothem/rules/token-budget-discipline.md +81 -0
- package/src/apothem/rules/token-efficiency-rewrite-protocol.md +79 -0
- package/src/apothem/rules/token-efficiency-rewrite.md +77 -0
- package/src/apothem/rules/tool-use-discipline.md +48 -0
- package/src/apothem/rules/visual-leverage.md +102 -0
- package/src/apothem/schemas/NOTICE.md +9 -0
- package/src/apothem/schemas/README.md +104 -0
- package/src/apothem/schemas/__init__.py +176 -0
- package/src/apothem/schemas/advisory-finding.schema.json +111 -0
- package/src/apothem/schemas/agent.schema.json +106 -0
- package/src/apothem/schemas/authorship-header.txt +1 -0
- package/src/apothem/schemas/cohort-manifest.yaml +248 -0
- package/src/apothem/schemas/cohort-metadata-vocabulary.yaml +168 -0
- package/src/apothem/schemas/cohort.schema.json +113 -0
- package/src/apothem/schemas/command.schema.json +68 -0
- package/src/apothem/schemas/compatibility-matrix.yaml +432 -0
- package/src/apothem/schemas/context-fragment.schema.json +64 -0
- package/src/apothem/schemas/freshness-token-denylist.txt +51 -0
- package/src/apothem/schemas/handoff-manifest.yaml +353 -0
- package/src/apothem/schemas/header-exceptions.txt +141 -0
- package/src/apothem/schemas/header-visibility.yaml +39 -0
- package/src/apothem/schemas/learning-signal.schema.json +46 -0
- package/src/apothem/schemas/memory-record.schema.json +61 -0
- package/src/apothem/schemas/output-style.schema.json +40 -0
- package/src/apothem/schemas/plan.schema.json +51 -0
- package/src/apothem/schemas/plugin.schema.json +83 -0
- package/src/apothem/schemas/profile.example.yaml +70 -0
- package/src/apothem/schemas/profile.minimal.yaml +6 -0
- package/src/apothem/schemas/profile.schema.json +396 -0
- package/src/apothem/schemas/reference-token-denylist.txt +25 -0
- package/src/apothem/schemas/skill.schema.json +75 -0
- package/src/apothem/skills/README.md +93 -0
- package/src/apothem/skills/dependency-upgrade/SKILL.md +105 -0
- package/src/apothem/skills/dev-toolkit/SKILL.md +120 -0
- package/src/apothem/skills/diagram-authoring/SKILL.md +113 -0
- package/src/apothem/skills/document-authoring/SKILL.md +118 -0
- package/src/apothem/skills/ecosystem-audit/SKILL.md +108 -0
- package/src/apothem/skills/ecosystem-audit/references/audit-fortress.md +85 -0
- package/src/apothem/skills/ecosystem-audit/references/procedure.md +162 -0
- package/src/apothem/skills/eval-harness/SKILL.md +88 -0
- package/src/apothem/skills/incident-runbook/SKILL.md +92 -0
- package/src/apothem/skills/multi-source-research/SKILL.md +90 -0
- package/src/apothem/skills/plan-suite/SKILL.md +118 -0
- package/src/apothem/skills/plan-suite/master_template.md +1324 -0
- package/src/apothem/skills/projectify/SKILL.md +117 -0
- package/src/apothem/skills/prompt-engineering/SKILL.md +122 -0
- package/src/apothem/skills/refactor-extract/SKILL.md +85 -0
- package/src/apothem/skills/research-suite/SKILL.md +170 -0
- package/src/apothem/skills/research-suite/references/directory-structure.md +47 -0
- package/src/apothem/skills/research-suite/references/lifecycle.md +67 -0
- package/src/apothem/skills/research-suite/references/principal-investigator-framework.md +37 -0
- package/src/apothem/skills/research-suite/references/rigor-mandates.md +30 -0
- package/src/apothem/skills/research-suite/research_template.md +476 -0
- package/src/apothem/skills/secret-rotation/SKILL.md +87 -0
- package/src/apothem/skills/source-synthesis/SKILL.md +92 -0
- package/src/apothem/skills/surgical-guard/SKILL.md +118 -0
- package/src/apothem/skills/test-authoring/SKILL.md +85 -0
- package/src/apothem/skills/vuln-triage/SKILL.md +91 -0
- package/src/apothem/skills/workflow/SKILL.md +139 -0
- package/src/apothem/statuslines/README.md +26 -0
- package/src/apothem/statuslines/__init__.py +20 -0
- package/src/apothem/statuslines/conformity.json +5 -0
- package/src/apothem/statuslines/render.py +334 -0
- package/src/apothem/statuslines/statusline.md +50 -0
- package/src/apothem/templates/README.md +43 -0
- package/src/apothem/templates/agents-md-template.md +80 -0
- package/src/apothem/templates/consideration-log.md +39 -0
- package/src/apothem/templates/expertise-gap-log.md +56 -0
- package/src/apothem/templates/master-index-template.md +93 -0
- package/src/apothem/templates/potency-map.md +53 -0
- package/src/apothem/templates/preservation-audit.md +60 -0
- package/src/apothem/templates/question-resolution-audit.md +52 -0
- package/src/apothem/templates/trace-matrix-template.md +77 -0
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
"""Unified conformance / security auditor — config scan, secret detection, conformance.
|
|
4
|
+
|
|
5
|
+
This module is the single auditor that subsumes the standalone conformity
|
|
6
|
+
validators and the machinable subset of the audit / review commands. It scans
|
|
7
|
+
a configuration file (or any text artifact), detects secret literals against a
|
|
8
|
+
closed, enumerated catalog, and applies rule-based conformance checks to the
|
|
9
|
+
parsed structure. Every result is a :class:`Finding`.
|
|
10
|
+
|
|
11
|
+
The auditor is **advisory by default**: :func:`audit` reports findings without
|
|
12
|
+
blocking, and the standalone CLI exits zero even with findings present unless
|
|
13
|
+
``--strict`` (or the ``APOTHEM_AUDITOR_STRICT`` environment opt-in) is set. Every
|
|
14
|
+
finding carries a definitive ``next_step`` — the determinant move the operator
|
|
15
|
+
can take. A malformed config yields a structured finding, never a crash; an
|
|
16
|
+
internal auditor failure is itself reported as a ``category = error`` finding,
|
|
17
|
+
never swallowed.
|
|
18
|
+
|
|
19
|
+
The serialized run (:meth:`Findings.to_dict`) validates against the bundled
|
|
20
|
+
``advisory-finding.schema.json`` contract, the same shape the hosted-layer
|
|
21
|
+
pull-request audit (:func:`pr_audit`) emits.
|
|
22
|
+
|
|
23
|
+
Standalone invocation (no package installation required; the plugin tree is
|
|
24
|
+
self-contained)::
|
|
25
|
+
|
|
26
|
+
python -m apothem.lib.auditor <path> [<path> ...] [--strict] [--json]
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import argparse
|
|
32
|
+
import json
|
|
33
|
+
import math
|
|
34
|
+
import os
|
|
35
|
+
import re
|
|
36
|
+
import sys
|
|
37
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
38
|
+
from dataclasses import dataclass, field
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Final, Literal, cast
|
|
41
|
+
|
|
42
|
+
from jsonschema import Draft202012Validator
|
|
43
|
+
|
|
44
|
+
from apothem.schemas import advisory_finding_schema_path
|
|
45
|
+
|
|
46
|
+
Category = Literal["config-scan", "secret", "conformance", "error"]
|
|
47
|
+
Severity = Literal["HIGH", "MEDIUM", "LOW"]
|
|
48
|
+
|
|
49
|
+
#: Opt-in environment variable that promotes the advisory default to strict.
|
|
50
|
+
STRICT_ENV: Final[str] = "APOTHEM_AUDITOR_STRICT"
|
|
51
|
+
_STRICT_TRUTHY: Final[frozenset[str]] = frozenset({"1", "true", "yes", "on"})
|
|
52
|
+
|
|
53
|
+
#: Config suffixes the structured scanner knows how to parse. Everything else is
|
|
54
|
+
#: still secret-scanned line-by-line as plain text.
|
|
55
|
+
_YAML_SUFFIXES: Final[frozenset[str]] = frozenset({".yaml", ".yml"})
|
|
56
|
+
_JSON_SUFFIXES: Final[frozenset[str]] = frozenset({".json"})
|
|
57
|
+
_TOML_SUFFIXES: Final[frozenset[str]] = frozenset({".toml"})
|
|
58
|
+
|
|
59
|
+
#: Substrings that mark a token as an obvious placeholder / example, not a
|
|
60
|
+
#: live secret, used to suppress the high-entropy heuristic's false positives.
|
|
61
|
+
_PLACEHOLDER_HINTS: Final[tuple[str, ...]] = (
|
|
62
|
+
"example",
|
|
63
|
+
"placeholder",
|
|
64
|
+
"dummy",
|
|
65
|
+
"redacted",
|
|
66
|
+
"your-",
|
|
67
|
+
"xxxx",
|
|
68
|
+
"changeme",
|
|
69
|
+
"<",
|
|
70
|
+
"...",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
#: Owner-identity strings that legitimately appear in authorship banners and must
|
|
74
|
+
#: never be flagged as secrets.
|
|
75
|
+
_IDENTITY_ALLOWLIST: Final[tuple[str, ...]] = (
|
|
76
|
+
"ahmedgad.com",
|
|
77
|
+
"me@ahmedgad.com",
|
|
78
|
+
"github.com/ahmed-g-gad",
|
|
79
|
+
"ahmed-g-gad",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
#: Universal-deny grant patterns: a conformance rule flags a config that grants
|
|
83
|
+
#: one of these in a permission / allow surface (the universal-deny floor).
|
|
84
|
+
_DENIED_GRANT_PATTERNS: Final[tuple[tuple[str, str], ...]] = (
|
|
85
|
+
(r"rm\s+-rf", "destructive recursive removal"),
|
|
86
|
+
(r"\bsudo\b", "privilege escalation"),
|
|
87
|
+
(r"git\s+push\s+--force", "history-rewriting force push"),
|
|
88
|
+
(r"\.env\b", "secret-file read access"),
|
|
89
|
+
(r"~/\.ssh", "ssh-credential read access"),
|
|
90
|
+
(r"\beval\b", "arbitrary code evaluation"),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class AuditorError(ValueError):
|
|
95
|
+
"""Raised when the auditor's own output fails its schema contract."""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class Location:
|
|
100
|
+
"""Where a finding was found."""
|
|
101
|
+
|
|
102
|
+
path: str
|
|
103
|
+
line: int | None = None
|
|
104
|
+
column: int | None = None
|
|
105
|
+
|
|
106
|
+
def to_dict(self) -> dict[str, object]:
|
|
107
|
+
"""Return the schema-shaped location mapping (omitting absent fields)."""
|
|
108
|
+
out: dict[str, object] = {"path": self.path}
|
|
109
|
+
if self.line is not None:
|
|
110
|
+
out["line"] = self.line
|
|
111
|
+
if self.column is not None:
|
|
112
|
+
out["column"] = self.column
|
|
113
|
+
return out
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class Finding:
|
|
118
|
+
"""A single advisory finding, shared by all three auditor capabilities."""
|
|
119
|
+
|
|
120
|
+
id: str
|
|
121
|
+
category: Category
|
|
122
|
+
severity: Severity
|
|
123
|
+
location: Location
|
|
124
|
+
message: str
|
|
125
|
+
next_step: str
|
|
126
|
+
|
|
127
|
+
def to_dict(self) -> dict[str, object]:
|
|
128
|
+
"""Return the schema-shaped finding mapping."""
|
|
129
|
+
return {
|
|
130
|
+
"id": self.id,
|
|
131
|
+
"category": self.category,
|
|
132
|
+
"severity": self.severity,
|
|
133
|
+
"location": self.location.to_dict(),
|
|
134
|
+
"message": self.message,
|
|
135
|
+
"next_step": self.next_step,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
def _order_key(self) -> tuple[str, int, str, str]:
|
|
139
|
+
"""Deterministic sort key: path, line, category, id."""
|
|
140
|
+
return (self.location.path, self.location.line or 0, self.category, self.id)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass(frozen=True)
|
|
144
|
+
class SecretPattern:
|
|
145
|
+
"""One entry in the closed secret-pattern catalog."""
|
|
146
|
+
|
|
147
|
+
label: str
|
|
148
|
+
match_rule: str
|
|
149
|
+
severity: Severity = "HIGH"
|
|
150
|
+
#: When ``True`` the rule is the Shannon-entropy heuristic, not a literal regex
|
|
151
|
+
#: match; the detector applies the entropy gate and placeholder suppression.
|
|
152
|
+
entropy: bool = False
|
|
153
|
+
_compiled: re.Pattern[str] = field(init=False, repr=False, compare=False)
|
|
154
|
+
|
|
155
|
+
def __post_init__(self) -> None:
|
|
156
|
+
object.__setattr__(self, "_compiled", re.compile(self.match_rule))
|
|
157
|
+
|
|
158
|
+
def search(self, text: str) -> Iterable[re.Match[str]]:
|
|
159
|
+
"""Yield every non-overlapping match of this pattern in *text*."""
|
|
160
|
+
return self._compiled.finditer(text)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
#: The closed secret-pattern catalog. Each pattern carries a ``label`` and a
|
|
164
|
+
#: ``match_rule``; together they are the exhaustive set the auditor detects.
|
|
165
|
+
#: Excluded classes are attested in :data:`SECRET_PATTERNS_NA`.
|
|
166
|
+
SECRET_PATTERNS: Final[tuple[SecretPattern, ...]] = (
|
|
167
|
+
SecretPattern("aws-access-key-id", r"\bAKIA[0-9A-Z]{16}\b"),
|
|
168
|
+
SecretPattern("github-personal-token", r"\bghp_[A-Za-z0-9]{36}\b"),
|
|
169
|
+
SecretPattern("github-oauth-token", r"\bgho_[A-Za-z0-9]{36}\b"),
|
|
170
|
+
SecretPattern("github-app-token", r"\b(?:ghs|ghu|ghr)_[A-Za-z0-9]{36}\b"),
|
|
171
|
+
SecretPattern("openai-style-api-key", r"\bsk-[A-Za-z0-9]{32,}\b"),
|
|
172
|
+
SecretPattern("google-api-key", r"\bAIza[0-9A-Za-z_\-]{35}\b"),
|
|
173
|
+
SecretPattern("slack-token", r"\bxox[abprs]-[A-Za-z0-9-]{10,}\b"),
|
|
174
|
+
SecretPattern(
|
|
175
|
+
"jwt",
|
|
176
|
+
r"\beyJ[A-Za-z0-9_\-]{10,}\.eyJ[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\b",
|
|
177
|
+
),
|
|
178
|
+
SecretPattern(
|
|
179
|
+
"pem-private-key",
|
|
180
|
+
r"-----BEGIN (?:RSA |OPENSSH |EC |DSA |PGP )?PRIVATE KEY-----",
|
|
181
|
+
),
|
|
182
|
+
SecretPattern(
|
|
183
|
+
"high-entropy-token",
|
|
184
|
+
r"[A-Za-z0-9_+/=\-]{40,}",
|
|
185
|
+
severity="LOW",
|
|
186
|
+
entropy=True,
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
#: Secret classes the catalog explicitly does NOT cover (the N/A attestation). A
|
|
191
|
+
#: downstream catalog-assembly step (the repo-wide sync) consumes this list when
|
|
192
|
+
#: deciding whether to widen coverage.
|
|
193
|
+
SECRET_PATTERNS_NA: Final[tuple[str, ...]] = (
|
|
194
|
+
"oauth-client-secrets (no distinctive prefix)",
|
|
195
|
+
"database-connection-strings (user:pass@host)",
|
|
196
|
+
"azure / microsoft cloud credentials",
|
|
197
|
+
"gcp-service-account-json keys",
|
|
198
|
+
"stripe / twilio api keys",
|
|
199
|
+
"ssh host keys / known_hosts entries",
|
|
200
|
+
"pkcs#8 / sec1 private keys outside the PEM armor",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
#: Minimum Shannon entropy (bits / char) for the high-entropy heuristic to fire.
|
|
204
|
+
_ENTROPY_THRESHOLD: Final[float] = 4.5
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@dataclass(frozen=True)
|
|
208
|
+
class Findings:
|
|
209
|
+
"""The result of an audit run — the schema-shaped output contract."""
|
|
210
|
+
|
|
211
|
+
findings: tuple[Finding, ...]
|
|
212
|
+
strict: bool
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def summary(self) -> dict[str, int]:
|
|
216
|
+
"""Aggregate finding counts by severity."""
|
|
217
|
+
high = sum(1 for f in self.findings if f.severity == "HIGH")
|
|
218
|
+
medium = sum(1 for f in self.findings if f.severity == "MEDIUM")
|
|
219
|
+
low = sum(1 for f in self.findings if f.severity == "LOW")
|
|
220
|
+
return {"total": len(self.findings), "high": high, "medium": medium, "low": low}
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def present(self) -> bool:
|
|
224
|
+
"""True when at least one finding was produced."""
|
|
225
|
+
return bool(self.findings)
|
|
226
|
+
|
|
227
|
+
def to_dict(self) -> dict[str, object]:
|
|
228
|
+
"""Return the full schema-shaped run mapping (``advisory-finding`` schema)."""
|
|
229
|
+
return {
|
|
230
|
+
"findings": [f.to_dict() for f in self.findings],
|
|
231
|
+
"strict": self.strict,
|
|
232
|
+
"summary": self.summary,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass(frozen=True)
|
|
237
|
+
class PrAuditInput:
|
|
238
|
+
"""The hosted-layer pull-request audit input (consumed by the Phase-B App).
|
|
239
|
+
|
|
240
|
+
The hosted layer hands the auditor the set of changed configuration paths
|
|
241
|
+
plus repository context (branch, base ref, repo identity); the auditor returns
|
|
242
|
+
the same :class:`Findings` shape the standalone CLI produces.
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
changed_paths: tuple[Path, ...]
|
|
246
|
+
repo_context: Mapping[str, object] = field(default_factory=dict)
|
|
247
|
+
strict: bool = False
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _schema() -> dict[str, object]:
|
|
251
|
+
"""Load and parse the advisory-finding output schema."""
|
|
252
|
+
raw = advisory_finding_schema_path().read_text(encoding="utf-8")
|
|
253
|
+
return cast("dict[str, object]", json.loads(raw))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def validate_findings(data: Mapping[str, object]) -> None:
|
|
257
|
+
"""Validate a serialized run against the advisory-finding schema.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
data: A :meth:`Findings.to_dict` mapping.
|
|
261
|
+
|
|
262
|
+
Raises:
|
|
263
|
+
AuditorError: When *data* violates the schema; the message lists every
|
|
264
|
+
validation error discovered.
|
|
265
|
+
"""
|
|
266
|
+
validator = Draft202012Validator(_schema())
|
|
267
|
+
errors = sorted(validator.iter_errors(dict(data)), key=lambda error: error.path)
|
|
268
|
+
if errors:
|
|
269
|
+
details = "; ".join(error.message for error in errors)
|
|
270
|
+
raise AuditorError(f"auditor output violates schema: {details}")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def resolve_strict(flag: bool) -> bool:
|
|
274
|
+
"""Resolve the effective strict mode from the CLI flag and the env opt-in.
|
|
275
|
+
|
|
276
|
+
Strict is enabled when the ``--strict`` flag is passed OR the
|
|
277
|
+
``APOTHEM_AUDITOR_STRICT`` environment variable is truthy. The shipped default
|
|
278
|
+
is advisory (``False``).
|
|
279
|
+
"""
|
|
280
|
+
if flag:
|
|
281
|
+
return True
|
|
282
|
+
return os.environ.get(STRICT_ENV, "").strip().lower() in _STRICT_TRUTHY
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _shannon_entropy(token: str) -> float:
|
|
286
|
+
"""Return the Shannon entropy (bits / char) of *token*."""
|
|
287
|
+
if not token:
|
|
288
|
+
return 0.0
|
|
289
|
+
counts: dict[str, int] = {}
|
|
290
|
+
for char in token:
|
|
291
|
+
counts[char] = counts.get(char, 0) + 1
|
|
292
|
+
length = len(token)
|
|
293
|
+
return -sum((c / length) * math.log2(c / length) for c in counts.values())
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _is_allowlisted(text: str) -> bool:
|
|
297
|
+
"""True when *text* contains an owner-identity allowlist token."""
|
|
298
|
+
return any(token in text for token in _IDENTITY_ALLOWLIST)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _looks_like_placeholder(token: str) -> bool:
|
|
302
|
+
"""True when *token* is an obvious example / placeholder, not a live secret."""
|
|
303
|
+
lowered = token.lower()
|
|
304
|
+
return any(hint in lowered for hint in _PLACEHOLDER_HINTS)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# --- Capability 1: configuration-file scanning ------------------------------
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def scan_config(path: Path) -> tuple[object | None, Finding | None]:
|
|
311
|
+
"""Parse a harness configuration file into an inspectable structure.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
path: The configuration file to read.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
A ``(structure, finding)`` pair. On success ``structure`` is the parsed
|
|
318
|
+
object and ``finding`` is ``None``. On a missing, unreadable, or malformed
|
|
319
|
+
file ``structure`` is ``None`` and ``finding`` is a structured
|
|
320
|
+
``config-scan`` finding — never a raised exception.
|
|
321
|
+
"""
|
|
322
|
+
if not path.is_file():
|
|
323
|
+
return None, Finding(
|
|
324
|
+
id="config-unreadable",
|
|
325
|
+
category="config-scan",
|
|
326
|
+
severity="MEDIUM",
|
|
327
|
+
location=Location(path=str(path)),
|
|
328
|
+
message=f"configuration path does not exist or is not a file: {path}",
|
|
329
|
+
next_step=f"create the file at {path} or correct the path passed to the auditor.",
|
|
330
|
+
)
|
|
331
|
+
try:
|
|
332
|
+
text = path.read_text(encoding="utf-8")
|
|
333
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
334
|
+
return None, Finding(
|
|
335
|
+
id="config-unreadable",
|
|
336
|
+
category="config-scan",
|
|
337
|
+
severity="MEDIUM",
|
|
338
|
+
location=Location(path=str(path)),
|
|
339
|
+
message=f"configuration file is not readable as UTF-8 text: {exc}",
|
|
340
|
+
next_step="verify the file is UTF-8 encoded and readable, then re-run the auditor.",
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
suffix = path.suffix.lower()
|
|
344
|
+
try:
|
|
345
|
+
structure = _parse_structure(text, suffix)
|
|
346
|
+
except _ParseError as exc:
|
|
347
|
+
return None, Finding(
|
|
348
|
+
id="config-malformed",
|
|
349
|
+
category="config-scan",
|
|
350
|
+
severity="MEDIUM",
|
|
351
|
+
location=Location(path=str(path), line=exc.line),
|
|
352
|
+
message=f"configuration file is malformed ({suffix or 'text'}): {exc}",
|
|
353
|
+
next_step="fix the reported syntax error; the auditor parses the file once it is well-formed.",
|
|
354
|
+
)
|
|
355
|
+
return structure, None
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class _ParseError(Exception):
|
|
359
|
+
"""Internal: a configuration file failed to parse."""
|
|
360
|
+
|
|
361
|
+
def __init__(self, message: str, line: int | None = None) -> None:
|
|
362
|
+
super().__init__(message)
|
|
363
|
+
self.line = line
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _parse_structure(text: str, suffix: str) -> object:
|
|
367
|
+
"""Parse *text* by *suffix*; raise :class:`_ParseError` on malformed input."""
|
|
368
|
+
if suffix in _JSON_SUFFIXES:
|
|
369
|
+
try:
|
|
370
|
+
return json.loads(text)
|
|
371
|
+
except json.JSONDecodeError as exc:
|
|
372
|
+
raise _ParseError(str(exc), exc.lineno) from exc
|
|
373
|
+
if suffix in _YAML_SUFFIXES:
|
|
374
|
+
import yaml # vendored; deferred so non-YAML scans never import it
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
return yaml.safe_load(text)
|
|
378
|
+
except yaml.YAMLError as exc:
|
|
379
|
+
raise _ParseError(str(exc)) from exc
|
|
380
|
+
if suffix in _TOML_SUFFIXES:
|
|
381
|
+
try:
|
|
382
|
+
# tomllib is stdlib only from 3.11; on 3.10 expose raw text.
|
|
383
|
+
# The dual-code ignore keeps both type-check contexts clean:
|
|
384
|
+
# import-not-found fires under --python-version=3.10 (typeshed
|
|
385
|
+
# gates tomllib to 3.11+), unused-ignore self-suppresses where
|
|
386
|
+
# the interpreter-version context resolves the stub.
|
|
387
|
+
import tomllib # type: ignore[import-not-found, unused-ignore]
|
|
388
|
+
except ModuleNotFoundError:
|
|
389
|
+
return {"_raw": text}
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
return tomllib.loads(text)
|
|
393
|
+
except tomllib.TOMLDecodeError as exc:
|
|
394
|
+
raise _ParseError(str(exc)) from exc
|
|
395
|
+
# Unknown / plain-text config: expose the raw text for line-level scanning.
|
|
396
|
+
return {"_raw": text}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# --- Capability 2: secret-pattern detection ---------------------------------
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def detect_secrets(path: Path, text: str) -> list[Finding]:
|
|
403
|
+
"""Detect secret literals in *text* against the closed catalog.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
path: The artifact the text came from (for finding locations).
|
|
407
|
+
text: The raw file content, scanned line by line.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
One finding per detected secret, deterministically ordered.
|
|
411
|
+
"""
|
|
412
|
+
findings: list[Finding] = []
|
|
413
|
+
for lineno, line in enumerate(text.splitlines(), start=1):
|
|
414
|
+
if _is_allowlisted(line):
|
|
415
|
+
continue
|
|
416
|
+
for pattern in SECRET_PATTERNS:
|
|
417
|
+
for match in pattern.search(line):
|
|
418
|
+
token = match.group(0)
|
|
419
|
+
if pattern.entropy:
|
|
420
|
+
if _looks_like_placeholder(token):
|
|
421
|
+
continue
|
|
422
|
+
if _shannon_entropy(token) < _ENTROPY_THRESHOLD:
|
|
423
|
+
continue
|
|
424
|
+
# Skip tokens a named pattern already owns (dedupe).
|
|
425
|
+
if _matched_by_named_pattern(token):
|
|
426
|
+
continue
|
|
427
|
+
findings.append(
|
|
428
|
+
Finding(
|
|
429
|
+
id=f"secret-{pattern.label}",
|
|
430
|
+
category="secret",
|
|
431
|
+
severity=pattern.severity,
|
|
432
|
+
location=Location(
|
|
433
|
+
path=str(path), line=lineno, column=match.start() + 1
|
|
434
|
+
),
|
|
435
|
+
message=f"possible {pattern.label} literal detected in {path.name}.",
|
|
436
|
+
next_step=(
|
|
437
|
+
"remove the literal; load the value from an environment variable "
|
|
438
|
+
"or secret manager and rotate the exposed credential."
|
|
439
|
+
),
|
|
440
|
+
)
|
|
441
|
+
)
|
|
442
|
+
return findings
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _matched_by_named_pattern(token: str) -> bool:
|
|
446
|
+
"""True when a non-entropy catalog pattern fully owns *token*."""
|
|
447
|
+
return any(
|
|
448
|
+
not p.entropy and p._compiled.fullmatch(token) is not None
|
|
449
|
+
for p in SECRET_PATTERNS
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# --- Capability 3: rule-based conformance -----------------------------------
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def run_conformance(path: Path, structure: object) -> list[Finding]:
|
|
457
|
+
"""Apply conformance rules to a parsed configuration structure.
|
|
458
|
+
|
|
459
|
+
The rules walk the parsed structure shape-agnostically (no per-harness schema
|
|
460
|
+
is assumed), so the same engine audits a JSON, YAML, or TOML config alike. The
|
|
461
|
+
shipped rule is the universal-deny floor: a permission / allow surface must not
|
|
462
|
+
grant a denied operation.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
path: The configuration file (for finding locations).
|
|
466
|
+
structure: The parsed structure from :func:`scan_config`.
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
One finding per conformance violation, deterministically ordered.
|
|
470
|
+
"""
|
|
471
|
+
findings: list[Finding] = []
|
|
472
|
+
for grant in _iter_string_values(structure):
|
|
473
|
+
for regex, label in _DENIED_GRANT_PATTERNS:
|
|
474
|
+
if re.search(regex, grant):
|
|
475
|
+
findings.append(
|
|
476
|
+
Finding(
|
|
477
|
+
id="denied-grant",
|
|
478
|
+
category="conformance",
|
|
479
|
+
severity="HIGH",
|
|
480
|
+
location=Location(path=str(path)),
|
|
481
|
+
message=f"configuration grants a denied operation ({label}): {grant!r}.",
|
|
482
|
+
next_step=(
|
|
483
|
+
f"remove the {label} grant; the universal-deny floor forbids it "
|
|
484
|
+
"regardless of the per-harness allow-list."
|
|
485
|
+
),
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
break
|
|
489
|
+
return findings
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _iter_string_values(structure: object) -> Iterable[str]:
|
|
493
|
+
"""Yield every string leaf in a nested mapping / sequence structure."""
|
|
494
|
+
if isinstance(structure, str):
|
|
495
|
+
yield structure
|
|
496
|
+
elif isinstance(structure, Mapping):
|
|
497
|
+
for value in structure.values():
|
|
498
|
+
yield from _iter_string_values(value)
|
|
499
|
+
elif isinstance(structure, (list, tuple)):
|
|
500
|
+
for item in structure:
|
|
501
|
+
yield from _iter_string_values(item)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
# --- Top-level audit + integration contracts --------------------------------
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _audit_path(path: Path) -> list[Finding]:
|
|
508
|
+
"""Run all three capabilities over a single path; never raise."""
|
|
509
|
+
findings: list[Finding] = []
|
|
510
|
+
try:
|
|
511
|
+
structure, scan_finding = scan_config(path)
|
|
512
|
+
if scan_finding is not None:
|
|
513
|
+
findings.append(scan_finding)
|
|
514
|
+
# Secret detection runs on raw text even when parsing failed.
|
|
515
|
+
if path.is_file():
|
|
516
|
+
try:
|
|
517
|
+
text = path.read_text(encoding="utf-8")
|
|
518
|
+
except (OSError, UnicodeDecodeError):
|
|
519
|
+
text = ""
|
|
520
|
+
findings.extend(detect_secrets(path, text))
|
|
521
|
+
if structure is not None:
|
|
522
|
+
findings.extend(run_conformance(path, structure))
|
|
523
|
+
except Exception as exc:
|
|
524
|
+
findings.append(
|
|
525
|
+
Finding(
|
|
526
|
+
id="auditor-internal-error",
|
|
527
|
+
category="error",
|
|
528
|
+
severity="MEDIUM",
|
|
529
|
+
location=Location(path=str(path)),
|
|
530
|
+
message=f"the auditor failed while scanning {path}: {exc}",
|
|
531
|
+
next_step="report this path to the auditor maintainer; the run continues for other paths.",
|
|
532
|
+
)
|
|
533
|
+
)
|
|
534
|
+
return findings
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def audit(paths: Sequence[Path], *, strict: bool = False) -> Findings:
|
|
538
|
+
"""Scan, detect, and conform over *paths*; return structured findings.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
paths: Configuration files (or directories — every contained file is
|
|
542
|
+
scanned) to audit.
|
|
543
|
+
strict: When ``True`` the result's ``strict`` flag is set so a CLI caller
|
|
544
|
+
exits non-zero on findings-present. Advisory (``False``) is the default.
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
A :class:`Findings` whose serialization validates against the
|
|
548
|
+
advisory-finding schema. The result is always returned — an internal error
|
|
549
|
+
on any path becomes a ``category = error`` finding, never an exception.
|
|
550
|
+
"""
|
|
551
|
+
collected: list[Finding] = []
|
|
552
|
+
for path in _expand_paths(paths):
|
|
553
|
+
collected.extend(_audit_path(path))
|
|
554
|
+
ordered = tuple(sorted(collected, key=Finding._order_key))
|
|
555
|
+
return Findings(findings=ordered, strict=strict)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def pr_audit(changed: PrAuditInput) -> Findings:
|
|
559
|
+
"""Audit the changed configuration paths of a pull request (hosted-layer contract).
|
|
560
|
+
|
|
561
|
+
This is the stable integration point the Phase-B hosted service consumes: it
|
|
562
|
+
receives the changed configuration paths plus repository context and returns
|
|
563
|
+
the same :class:`Findings` shape the standalone :func:`audit` produces. The
|
|
564
|
+
repository context is carried for the caller's reporting; the audit itself runs
|
|
565
|
+
over the changed paths.
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
changed: The changed configuration paths and repository context.
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
Findings over the changed paths, in the advisory-finding shape.
|
|
572
|
+
"""
|
|
573
|
+
return audit(changed.changed_paths, strict=changed.strict)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _expand_paths(paths: Sequence[Path]) -> list[Path]:
|
|
577
|
+
"""Expand directories to their contained files; keep files as-is, sorted."""
|
|
578
|
+
expanded: list[Path] = []
|
|
579
|
+
for path in paths:
|
|
580
|
+
if path.is_dir():
|
|
581
|
+
expanded.extend(sorted(p for p in path.rglob("*") if p.is_file()))
|
|
582
|
+
else:
|
|
583
|
+
expanded.append(path)
|
|
584
|
+
return expanded
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _exit_code(findings_present: bool, *, strict: bool) -> int:
|
|
588
|
+
"""Map a verdict to an exit code: advisory exits 0; strict exits 1 on findings."""
|
|
589
|
+
if findings_present and strict:
|
|
590
|
+
return 1
|
|
591
|
+
return 0
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
595
|
+
"""Build the standalone CLI argument parser."""
|
|
596
|
+
parser = argparse.ArgumentParser(
|
|
597
|
+
prog="python -m apothem.lib.auditor",
|
|
598
|
+
description="Advisory conformance / security auditor: config scan, secret detection, conformance.",
|
|
599
|
+
)
|
|
600
|
+
parser.add_argument(
|
|
601
|
+
"paths",
|
|
602
|
+
nargs="+",
|
|
603
|
+
type=Path,
|
|
604
|
+
help="configuration files or directories to audit",
|
|
605
|
+
)
|
|
606
|
+
parser.add_argument(
|
|
607
|
+
"--strict",
|
|
608
|
+
action="store_true",
|
|
609
|
+
help="exit non-zero when findings are present (default: advisory, always exit 0)",
|
|
610
|
+
)
|
|
611
|
+
parser.add_argument(
|
|
612
|
+
"--json",
|
|
613
|
+
action="store_true",
|
|
614
|
+
help="emit the findings as JSON (default: human-readable advisory summary)",
|
|
615
|
+
)
|
|
616
|
+
return parser
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _render_human(result: Findings) -> str:
|
|
620
|
+
"""Render a human-readable advisory summary of the run."""
|
|
621
|
+
lines: list[str] = []
|
|
622
|
+
summary = result.summary
|
|
623
|
+
if not result.present:
|
|
624
|
+
return "auditor: 0 findings — configuration is clean."
|
|
625
|
+
lines.append(
|
|
626
|
+
f"auditor: {summary['total']} finding(s) "
|
|
627
|
+
f"(HIGH {summary['high']} | MEDIUM {summary['medium']} | LOW {summary['low']}) -- advisory."
|
|
628
|
+
)
|
|
629
|
+
for finding in result.findings:
|
|
630
|
+
loc = finding.location
|
|
631
|
+
where = loc.path + (f":{loc.line}" if loc.line is not None else "")
|
|
632
|
+
lines.append(
|
|
633
|
+
f" [{finding.severity}] {finding.category}/{finding.id} @ {where}"
|
|
634
|
+
)
|
|
635
|
+
lines.append(f" {finding.message}")
|
|
636
|
+
lines.append(f" next step: {finding.next_step}")
|
|
637
|
+
return "\n".join(lines)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
641
|
+
"""Standalone CLI entry point (``python -m apothem.lib.auditor``).
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
argv: The argument vector (defaults to ``sys.argv[1:]``).
|
|
645
|
+
|
|
646
|
+
Returns:
|
|
647
|
+
``0`` in advisory mode (the shipped default), even with findings present;
|
|
648
|
+
``1`` when ``--strict`` (or the env opt-in) is set and findings are present.
|
|
649
|
+
"""
|
|
650
|
+
parser = _build_parser()
|
|
651
|
+
args = parser.parse_args(argv)
|
|
652
|
+
strict = resolve_strict(bool(args.strict))
|
|
653
|
+
result = audit(cast("list[Path]", args.paths), strict=strict)
|
|
654
|
+
serialized = result.to_dict()
|
|
655
|
+
validate_findings(serialized)
|
|
656
|
+
if args.json:
|
|
657
|
+
sys.stdout.write(json.dumps(serialized, indent=2, sort_keys=True) + "\n")
|
|
658
|
+
else:
|
|
659
|
+
sys.stdout.write(_render_human(result) + "\n")
|
|
660
|
+
return _exit_code(result.present, strict=strict)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
__all__ = [
|
|
664
|
+
"SECRET_PATTERNS",
|
|
665
|
+
"SECRET_PATTERNS_NA",
|
|
666
|
+
"STRICT_ENV",
|
|
667
|
+
"AuditorError",
|
|
668
|
+
"Category",
|
|
669
|
+
"Finding",
|
|
670
|
+
"Findings",
|
|
671
|
+
"Location",
|
|
672
|
+
"PrAuditInput",
|
|
673
|
+
"SecretPattern",
|
|
674
|
+
"Severity",
|
|
675
|
+
"audit",
|
|
676
|
+
"detect_secrets",
|
|
677
|
+
"main",
|
|
678
|
+
"pr_audit",
|
|
679
|
+
"resolve_strict",
|
|
680
|
+
"run_conformance",
|
|
681
|
+
"scan_config",
|
|
682
|
+
"validate_findings",
|
|
683
|
+
]
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
if __name__ == "__main__": # pragma: no cover — exercised via subprocess in tests
|
|
687
|
+
raise SystemExit(main(sys.argv[1:]))
|