bps-kit 1.0.1 → 1.0.2
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/package.json +1 -1
- package/templates/.agents/agents/backend-specialist.md +263 -0
- package/templates/.agents/agents/code-archaeologist.md +106 -0
- package/templates/.agents/agents/database-architect.md +226 -0
- package/templates/.agents/agents/debugger.md +225 -0
- package/templates/.agents/agents/devops-engineer.md +242 -0
- package/templates/.agents/agents/documentation-writer.md +104 -0
- package/templates/.agents/agents/explorer-agent.md +73 -0
- package/templates/.agents/agents/frontend-specialist.md +593 -0
- package/templates/.agents/agents/game-developer.md +162 -0
- package/templates/.agents/agents/mobile-developer.md +377 -0
- package/templates/.agents/agents/orchestrator.md +416 -0
- package/templates/.agents/agents/penetration-tester.md +188 -0
- package/templates/.agents/agents/performance-optimizer.md +187 -0
- package/templates/.agents/agents/product-manager.md +112 -0
- package/templates/.agents/agents/product-owner.md +95 -0
- package/templates/.agents/agents/project-planner.md +406 -0
- package/templates/.agents/agents/qa-automation-engineer.md +103 -0
- package/templates/.agents/agents/security-auditor.md +170 -0
- package/templates/.agents/agents/seo-specialist.md +111 -0
- package/templates/.agents/agents/test-engineer.md +158 -0
- package/templates/.agents/rules/GEMINI.md +219 -0
- package/templates/.agents/scripts/auto_preview.py +148 -0
- package/templates/.agents/scripts/checklist.py +217 -0
- package/templates/.agents/scripts/session_manager.py +120 -0
- package/templates/.agents/scripts/verify_all.py +327 -0
- package/templates/.agents/workflows/brainstorm.md +113 -0
- package/templates/.agents/workflows/create.md +59 -0
- package/templates/.agents/workflows/debug.md +103 -0
- package/templates/.agents/workflows/deploy.md +176 -0
- package/templates/.agents/workflows/enhance.md +63 -0
- package/templates/.agents/workflows/orchestrate.md +237 -0
- package/templates/.agents/workflows/plan.md +89 -0
- package/templates/.agents/workflows/preview.md +81 -0
- package/templates/.agents/workflows/setup-brain.md +39 -0
- package/templates/.agents/workflows/status.md +86 -0
- package/templates/.agents/workflows/test.md +144 -0
- package/templates/.agents/workflows/ui-ux-pro-max.md +296 -0
- package/templates/skills_normal/api-patterns/scripts/api_validator.py +211 -0
- package/templates/skills_normal/database-design/scripts/schema_validator.py +172 -0
- package/templates/skills_normal/frontend-design/scripts/accessibility_checker.py +183 -0
- package/templates/skills_normal/frontend-design/scripts/ux_audit.py +722 -0
- package/templates/skills_normal/git-pushing/scripts/smart_commit.sh +19 -0
- package/templates/skills_normal/lint-and-validate/scripts/lint_runner.py +184 -0
- package/templates/skills_normal/lint-and-validate/scripts/type_coverage.py +173 -0
- package/templates/skills_normal/performance-profiling/scripts/lighthouse_audit.py +76 -0
- package/templates/skills_normal/senior-fullstack/scripts/code_quality_analyzer.py +114 -0
- package/templates/skills_normal/senior-fullstack/scripts/fullstack_scaffolder.py +114 -0
- package/templates/skills_normal/senior-fullstack/scripts/project_scaffolder.py +114 -0
- package/templates/skills_normal/seo-fundamentals/scripts/seo_checker.py +219 -0
- package/templates/skills_normal/testing-patterns/scripts/test_runner.py +219 -0
- package/templates/skills_normal/vulnerability-scanner/scripts/security_scan.py +458 -0
- package/templates/vault/007/scripts/config.py +472 -0
- package/templates/vault/007/scripts/full_audit.py +1306 -0
- package/templates/vault/007/scripts/quick_scan.py +481 -0
- package/templates/vault/007/scripts/requirements.txt +26 -0
- package/templates/vault/007/scripts/scanners/__init__.py +0 -0
- package/templates/vault/007/scripts/scanners/dependency_scanner.py +1305 -0
- package/templates/vault/007/scripts/scanners/injection_scanner.py +1104 -0
- package/templates/vault/007/scripts/scanners/secrets_scanner.py +1008 -0
- package/templates/vault/007/scripts/score_calculator.py +693 -0
- package/templates/vault/agent-orchestrator/scripts/match_skills.py +329 -0
- package/templates/vault/agent-orchestrator/scripts/orchestrate.py +304 -0
- package/templates/vault/agent-orchestrator/scripts/requirements.txt +1 -0
- package/templates/vault/agent-orchestrator/scripts/scan_registry.py +508 -0
- package/templates/vault/ai-studio-image/scripts/config.py +613 -0
- package/templates/vault/ai-studio-image/scripts/generate.py +630 -0
- package/templates/vault/ai-studio-image/scripts/prompt_engine.py +424 -0
- package/templates/vault/ai-studio-image/scripts/requirements.txt +4 -0
- package/templates/vault/ai-studio-image/scripts/templates.py +349 -0
- package/templates/vault/android_ui_verification/scripts/verify_ui.sh +32 -0
- package/templates/vault/apify-audience-analysis/reference/scripts/run_actor.js +363 -0
- package/templates/vault/apify-brand-reputation-monitoring/reference/scripts/run_actor.js +363 -0
- package/templates/vault/apify-competitor-intelligence/reference/scripts/run_actor.js +363 -0
- package/templates/vault/apify-content-analytics/reference/scripts/run_actor.js +363 -0
- package/templates/vault/apify-ecommerce/reference/scripts/package.json +3 -0
- package/templates/vault/apify-ecommerce/reference/scripts/run_actor.js +369 -0
- package/templates/vault/apify-influencer-discovery/reference/scripts/run_actor.js +363 -0
- package/templates/vault/apify-lead-generation/reference/scripts/run_actor.js +363 -0
- package/templates/vault/apify-market-research/reference/scripts/run_actor.js +363 -0
- package/templates/vault/apify-trend-analysis/reference/scripts/run_actor.js +363 -0
- package/templates/vault/apify-ultimate-scraper/reference/scripts/run_actor.js +363 -0
- package/templates/vault/audio-transcriber/scripts/install-requirements.sh +190 -0
- package/templates/vault/audio-transcriber/scripts/transcribe.py +486 -0
- package/templates/vault/claude-monitor/scripts/api_bench.py +240 -0
- package/templates/vault/claude-monitor/scripts/config.py +69 -0
- package/templates/vault/claude-monitor/scripts/health_check.py +362 -0
- package/templates/vault/claude-monitor/scripts/monitor.py +296 -0
- package/templates/vault/content-creator/scripts/brand_voice_analyzer.py +185 -0
- package/templates/vault/content-creator/scripts/seo_optimizer.py +419 -0
- package/templates/vault/context-agent/scripts/active_context.py +227 -0
- package/templates/vault/context-agent/scripts/compressor.py +149 -0
- package/templates/vault/context-agent/scripts/config.py +69 -0
- package/templates/vault/context-agent/scripts/context_loader.py +155 -0
- package/templates/vault/context-agent/scripts/context_manager.py +302 -0
- package/templates/vault/context-agent/scripts/models.py +103 -0
- package/templates/vault/context-agent/scripts/project_registry.py +132 -0
- package/templates/vault/context-agent/scripts/requirements.txt +6 -0
- package/templates/vault/context-agent/scripts/search.py +115 -0
- package/templates/vault/context-agent/scripts/session_parser.py +206 -0
- package/templates/vault/context-agent/scripts/session_summary.py +319 -0
- package/templates/vault/context-guardian/scripts/context_snapshot.py +229 -0
- package/templates/vault/docx/ooxml/scripts/pack.py +159 -0
- package/templates/vault/docx/ooxml/scripts/unpack.py +29 -0
- package/templates/vault/docx/ooxml/scripts/validate.py +69 -0
- package/templates/vault/docx/ooxml/scripts/validation/__init__.py +15 -0
- package/templates/vault/docx/ooxml/scripts/validation/base.py +951 -0
- package/templates/vault/docx/ooxml/scripts/validation/docx.py +274 -0
- package/templates/vault/docx/ooxml/scripts/validation/pptx.py +315 -0
- package/templates/vault/docx/ooxml/scripts/validation/redlining.py +279 -0
- package/templates/vault/docx/scripts/__init__.py +1 -0
- package/templates/vault/docx/scripts/document.py +1276 -0
- package/templates/vault/docx/scripts/templates/comments.xml +3 -0
- package/templates/vault/docx/scripts/templates/commentsExtended.xml +3 -0
- package/templates/vault/docx/scripts/templates/commentsExtensible.xml +3 -0
- package/templates/vault/docx/scripts/templates/commentsIds.xml +3 -0
- package/templates/vault/docx/scripts/templates/people.xml +3 -0
- package/templates/vault/docx/scripts/utilities.py +374 -0
- package/templates/vault/docx-official/ooxml/scripts/pack.py +159 -0
- package/templates/vault/docx-official/ooxml/scripts/unpack.py +29 -0
- package/templates/vault/docx-official/ooxml/scripts/validate.py +69 -0
- package/templates/vault/docx-official/ooxml/scripts/validation/__init__.py +15 -0
- package/templates/vault/docx-official/ooxml/scripts/validation/base.py +951 -0
- package/templates/vault/docx-official/ooxml/scripts/validation/docx.py +274 -0
- package/templates/vault/docx-official/ooxml/scripts/validation/pptx.py +315 -0
- package/templates/vault/docx-official/ooxml/scripts/validation/redlining.py +279 -0
- package/templates/vault/docx-official/scripts/__init__.py +1 -0
- package/templates/vault/docx-official/scripts/document.py +1276 -0
- package/templates/vault/docx-official/scripts/templates/comments.xml +3 -0
- package/templates/vault/docx-official/scripts/templates/commentsExtended.xml +3 -0
- package/templates/vault/docx-official/scripts/templates/commentsExtensible.xml +3 -0
- package/templates/vault/docx-official/scripts/templates/commentsIds.xml +3 -0
- package/templates/vault/docx-official/scripts/templates/people.xml +3 -0
- package/templates/vault/docx-official/scripts/utilities.py +374 -0
- package/templates/vault/geo-fundamentals/scripts/geo_checker.py +289 -0
- package/templates/vault/helm-chart-scaffolding/scripts/validate-chart.sh +244 -0
- package/templates/vault/i18n-localization/scripts/i18n_checker.py +241 -0
- package/templates/vault/instagram/scripts/account_setup.py +233 -0
- package/templates/vault/instagram/scripts/analyze.py +221 -0
- package/templates/vault/instagram/scripts/api_client.py +444 -0
- package/templates/vault/instagram/scripts/auth.py +411 -0
- package/templates/vault/instagram/scripts/comments.py +160 -0
- package/templates/vault/instagram/scripts/config.py +111 -0
- package/templates/vault/instagram/scripts/db.py +467 -0
- package/templates/vault/instagram/scripts/export.py +138 -0
- package/templates/vault/instagram/scripts/governance.py +233 -0
- package/templates/vault/instagram/scripts/hashtags.py +114 -0
- package/templates/vault/instagram/scripts/insights.py +170 -0
- package/templates/vault/instagram/scripts/media.py +65 -0
- package/templates/vault/instagram/scripts/messages.py +103 -0
- package/templates/vault/instagram/scripts/profile.py +58 -0
- package/templates/vault/instagram/scripts/publish.py +449 -0
- package/templates/vault/instagram/scripts/requirements.txt +5 -0
- package/templates/vault/instagram/scripts/run_all.py +189 -0
- package/templates/vault/instagram/scripts/schedule.py +189 -0
- package/templates/vault/instagram/scripts/serve_api.py +234 -0
- package/templates/vault/instagram/scripts/templates.py +155 -0
- package/templates/vault/junta-leiloeiros/scripts/db.py +216 -0
- package/templates/vault/junta-leiloeiros/scripts/export.py +137 -0
- package/templates/vault/junta-leiloeiros/scripts/requirements.txt +15 -0
- package/templates/vault/junta-leiloeiros/scripts/run_all.py +190 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/__init__.py +4 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/base_scraper.py +209 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/generic_scraper.py +110 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucap.py +110 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/juceac.py +72 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/juceal.py +72 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/juceb.py +68 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucec.py +63 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucema.py +211 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucemg.py +218 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucep.py +70 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucepa.py +74 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucepar.py +80 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucepe.py +78 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucepi.py +69 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucer.py +256 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucerja.py +170 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucern.py +71 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucesc.py +89 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucesp.py +233 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucetins.py +134 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucis_df.py +63 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/jucisrs.py +299 -0
- package/templates/vault/junta-leiloeiros/scripts/scraper/states.py +99 -0
- package/templates/vault/junta-leiloeiros/scripts/serve_api.py +164 -0
- package/templates/vault/junta-leiloeiros/scripts/web_scraper_fallback.py +233 -0
- package/templates/vault/last30days/scripts/last30days.py +521 -0
- package/templates/vault/last30days/scripts/lib/__init__.py +1 -0
- package/templates/vault/last30days/scripts/lib/cache.py +152 -0
- package/templates/vault/last30days/scripts/lib/dates.py +124 -0
- package/templates/vault/last30days/scripts/lib/dedupe.py +120 -0
- package/templates/vault/last30days/scripts/lib/env.py +149 -0
- package/templates/vault/last30days/scripts/lib/http.py +152 -0
- package/templates/vault/last30days/scripts/lib/models.py +175 -0
- package/templates/vault/last30days/scripts/lib/normalize.py +160 -0
- package/templates/vault/last30days/scripts/lib/openai_reddit.py +230 -0
- package/templates/vault/last30days/scripts/lib/reddit_enrich.py +232 -0
- package/templates/vault/last30days/scripts/lib/render.py +383 -0
- package/templates/vault/last30days/scripts/lib/schema.py +336 -0
- package/templates/vault/last30days/scripts/lib/score.py +311 -0
- package/templates/vault/last30days/scripts/lib/ui.py +324 -0
- package/templates/vault/last30days/scripts/lib/websearch.py +401 -0
- package/templates/vault/last30days/scripts/lib/xai_x.py +217 -0
- package/templates/vault/leiloeiro-avaliacao/scripts/governance.py +106 -0
- package/templates/vault/leiloeiro-avaliacao/scripts/requirements.txt +1 -0
- package/templates/vault/leiloeiro-edital/scripts/governance.py +106 -0
- package/templates/vault/leiloeiro-edital/scripts/requirements.txt +1 -0
- package/templates/vault/leiloeiro-ia/scripts/governance.py +106 -0
- package/templates/vault/leiloeiro-ia/scripts/requirements.txt +1 -0
- package/templates/vault/leiloeiro-juridico/scripts/governance.py +106 -0
- package/templates/vault/leiloeiro-juridico/scripts/requirements.txt +1 -0
- package/templates/vault/leiloeiro-mercado/scripts/governance.py +106 -0
- package/templates/vault/leiloeiro-mercado/scripts/requirements.txt +1 -0
- package/templates/vault/leiloeiro-risco/scripts/governance.py +106 -0
- package/templates/vault/leiloeiro-risco/scripts/requirements.txt +1 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/db/database.ts +24 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/db/db.ts +35 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/db/index.ts +2 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/db/migrations.ts +31 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/db/schema.sql +8 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/index.ts +44 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/routes/todos.ts +155 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/backend/src/types/index.ts +35 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/App.css +384 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/App.tsx +81 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/api/todos.ts +57 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/components/ConfirmDialog.tsx +26 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/components/EmptyState.tsx +8 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/components/TodoForm.tsx +43 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/components/TodoItem.tsx +36 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/components/TodoList.tsx +27 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/hooks/useTodos.ts +81 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/index.css +48 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/main.tsx +10 -0
- package/templates/vault/loki-mode/examples/todo-app-generated/frontend/src/vite-env.d.ts +1 -0
- package/templates/vault/loki-mode/scripts/export-to-vibe-kanban.sh +178 -0
- package/templates/vault/loki-mode/scripts/loki-wrapper.sh +281 -0
- package/templates/vault/loki-mode/scripts/take-screenshots.js +55 -0
- package/templates/vault/matematico-tao/scripts/complexity_analyzer.py +544 -0
- package/templates/vault/matematico-tao/scripts/dependency_graph.py +538 -0
- package/templates/vault/mcp-builder/scripts/connections.py +151 -0
- package/templates/vault/mcp-builder/scripts/evaluation.py +373 -0
- package/templates/vault/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/templates/vault/mcp-builder/scripts/requirements.txt +2 -0
- package/templates/vault/mobile-design/scripts/mobile_audit.py +670 -0
- package/templates/vault/notebooklm/scripts/__init__.py +81 -0
- package/templates/vault/notebooklm/scripts/ask_question.py +256 -0
- package/templates/vault/notebooklm/scripts/auth_manager.py +358 -0
- package/templates/vault/notebooklm/scripts/browser_session.py +255 -0
- package/templates/vault/notebooklm/scripts/browser_utils.py +107 -0
- package/templates/vault/notebooklm/scripts/cleanup_manager.py +302 -0
- package/templates/vault/notebooklm/scripts/config.py +44 -0
- package/templates/vault/notebooklm/scripts/notebook_manager.py +410 -0
- package/templates/vault/notebooklm/scripts/run.py +102 -0
- package/templates/vault/notebooklm/scripts/setup_environment.py +204 -0
- package/templates/vault/pdf/scripts/check_bounding_boxes.py +70 -0
- package/templates/vault/pdf/scripts/check_bounding_boxes_test.py +226 -0
- package/templates/vault/pdf/scripts/check_fillable_fields.py +12 -0
- package/templates/vault/pdf/scripts/convert_pdf_to_images.py +35 -0
- package/templates/vault/pdf/scripts/create_validation_image.py +41 -0
- package/templates/vault/pdf/scripts/extract_form_field_info.py +152 -0
- package/templates/vault/pdf/scripts/fill_fillable_fields.py +114 -0
- package/templates/vault/pdf/scripts/fill_pdf_form_with_annotations.py +108 -0
- package/templates/vault/pdf-official/scripts/check_bounding_boxes.py +70 -0
- package/templates/vault/pdf-official/scripts/check_bounding_boxes_test.py +226 -0
- package/templates/vault/pdf-official/scripts/check_fillable_fields.py +12 -0
- package/templates/vault/pdf-official/scripts/convert_pdf_to_images.py +35 -0
- package/templates/vault/pdf-official/scripts/create_validation_image.py +41 -0
- package/templates/vault/pdf-official/scripts/extract_form_field_info.py +152 -0
- package/templates/vault/pdf-official/scripts/fill_fillable_fields.py +114 -0
- package/templates/vault/pdf-official/scripts/fill_pdf_form_with_annotations.py +108 -0
- package/templates/vault/planning-with-files/scripts/check-complete.sh +44 -0
- package/templates/vault/planning-with-files/scripts/init-session.sh +120 -0
- package/templates/vault/pptx/ooxml/scripts/pack.py +159 -0
- package/templates/vault/pptx/ooxml/scripts/unpack.py +29 -0
- package/templates/vault/pptx/ooxml/scripts/validate.py +69 -0
- package/templates/vault/pptx/ooxml/scripts/validation/__init__.py +15 -0
- package/templates/vault/pptx/ooxml/scripts/validation/base.py +951 -0
- package/templates/vault/pptx/ooxml/scripts/validation/docx.py +274 -0
- package/templates/vault/pptx/ooxml/scripts/validation/pptx.py +315 -0
- package/templates/vault/pptx/ooxml/scripts/validation/redlining.py +279 -0
- package/templates/vault/pptx/scripts/html2pptx.js +979 -0
- package/templates/vault/pptx/scripts/inventory.py +1020 -0
- package/templates/vault/pptx/scripts/rearrange.py +231 -0
- package/templates/vault/pptx/scripts/replace.py +385 -0
- package/templates/vault/pptx/scripts/thumbnail.py +450 -0
- package/templates/vault/pptx-official/ooxml/scripts/pack.py +159 -0
- package/templates/vault/pptx-official/ooxml/scripts/unpack.py +29 -0
- package/templates/vault/pptx-official/ooxml/scripts/validate.py +69 -0
- package/templates/vault/pptx-official/ooxml/scripts/validation/__init__.py +15 -0
- package/templates/vault/pptx-official/ooxml/scripts/validation/base.py +951 -0
- package/templates/vault/pptx-official/ooxml/scripts/validation/docx.py +274 -0
- package/templates/vault/pptx-official/ooxml/scripts/validation/pptx.py +315 -0
- package/templates/vault/pptx-official/ooxml/scripts/validation/redlining.py +279 -0
- package/templates/vault/pptx-official/scripts/html2pptx.js +979 -0
- package/templates/vault/pptx-official/scripts/inventory.py +1020 -0
- package/templates/vault/pptx-official/scripts/rearrange.py +231 -0
- package/templates/vault/pptx-official/scripts/replace.py +385 -0
- package/templates/vault/pptx-official/scripts/thumbnail.py +450 -0
- package/templates/vault/product-manager-toolkit/scripts/customer_interview_analyzer.py +441 -0
- package/templates/vault/product-manager-toolkit/scripts/rice_prioritizer.py +296 -0
- package/templates/vault/prompt-engineering-patterns/scripts/optimize-prompt.py +279 -0
- package/templates/vault/scripts/.skill_cache.json +7538 -0
- package/templates/vault/scripts/skill_search.py +228 -0
- package/templates/vault/senior-architect/scripts/architecture_diagram_generator.py +114 -0
- package/templates/vault/senior-architect/scripts/dependency_analyzer.py +114 -0
- package/templates/vault/senior-architect/scripts/project_architect.py +114 -0
- package/templates/vault/shopify-development/scripts/requirements.txt +19 -0
- package/templates/vault/shopify-development/scripts/shopify_graphql.py +428 -0
- package/templates/vault/shopify-development/scripts/shopify_init.py +441 -0
- package/templates/vault/shopify-development/scripts/tests/test_shopify_init.py +379 -0
- package/templates/vault/skill-creator/scripts/init_skill.py +303 -0
- package/templates/vault/skill-creator/scripts/package_skill.py +110 -0
- package/templates/vault/skill-creator/scripts/quick_validate.py +95 -0
- package/templates/vault/skill-installer/scripts/detect_skills.py +318 -0
- package/templates/vault/skill-installer/scripts/install_skill.py +1708 -0
- package/templates/vault/skill-installer/scripts/package_skill.py +417 -0
- package/templates/vault/skill-installer/scripts/requirements.txt +1 -0
- package/templates/vault/skill-installer/scripts/validate_skill.py +430 -0
- package/templates/vault/skill-sentinel/scripts/analyzers/__init__.py +13 -0
- package/templates/vault/skill-sentinel/scripts/analyzers/code_quality.py +247 -0
- package/templates/vault/skill-sentinel/scripts/analyzers/cross_skill.py +134 -0
- package/templates/vault/skill-sentinel/scripts/analyzers/dependencies.py +121 -0
- package/templates/vault/skill-sentinel/scripts/analyzers/documentation.py +189 -0
- package/templates/vault/skill-sentinel/scripts/analyzers/governance_audit.py +153 -0
- package/templates/vault/skill-sentinel/scripts/analyzers/performance.py +164 -0
- package/templates/vault/skill-sentinel/scripts/analyzers/security.py +189 -0
- package/templates/vault/skill-sentinel/scripts/config.py +158 -0
- package/templates/vault/skill-sentinel/scripts/cost_optimizer.py +146 -0
- package/templates/vault/skill-sentinel/scripts/db.py +354 -0
- package/templates/vault/skill-sentinel/scripts/governance.py +58 -0
- package/templates/vault/skill-sentinel/scripts/recommender.py +228 -0
- package/templates/vault/skill-sentinel/scripts/report_generator.py +224 -0
- package/templates/vault/skill-sentinel/scripts/requirements.txt +1 -0
- package/templates/vault/skill-sentinel/scripts/run_audit.py +290 -0
- package/templates/vault/skill-sentinel/scripts/scanner.py +271 -0
- package/templates/vault/stability-ai/scripts/config.py +266 -0
- package/templates/vault/stability-ai/scripts/generate.py +687 -0
- package/templates/vault/stability-ai/scripts/requirements.txt +4 -0
- package/templates/vault/stability-ai/scripts/styles.py +174 -0
- package/templates/vault/telegram/assets/boilerplate/nodejs/src/bot-client.ts +86 -0
- package/templates/vault/telegram/assets/boilerplate/nodejs/src/handlers.ts +79 -0
- package/templates/vault/telegram/assets/boilerplate/nodejs/src/index.ts +32 -0
- package/templates/vault/telegram/scripts/send_message.py +143 -0
- package/templates/vault/telegram/scripts/setup_project.py +103 -0
- package/templates/vault/telegram/scripts/test_bot.py +144 -0
- package/templates/vault/typescript-expert/scripts/ts_diagnostic.py +203 -0
- package/templates/vault/ui-ux-pro-max/scripts/__pycache__/core.cpython-314.pyc +0 -0
- package/templates/vault/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-314.pyc +0 -0
- package/templates/vault/ui-ux-pro-max/scripts/core.py +257 -0
- package/templates/vault/ui-ux-pro-max/scripts/design_system.py +487 -0
- package/templates/vault/ui-ux-pro-max/scripts/search.py +76 -0
- package/templates/vault/videodb/scripts/ws_listener.py +204 -0
- package/templates/vault/web-artifacts-builder/scripts/bundle-artifact.sh +54 -0
- package/templates/vault/web-artifacts-builder/scripts/init-artifact.sh +322 -0
- package/templates/vault/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/templates/vault/webapp-testing/scripts/with_server.py +106 -0
- package/templates/vault/whatsapp-cloud-api/assets/boilerplate/nodejs/src/index.ts +125 -0
- package/templates/vault/whatsapp-cloud-api/assets/boilerplate/nodejs/src/template-manager.ts +67 -0
- package/templates/vault/whatsapp-cloud-api/assets/boilerplate/nodejs/src/types.ts +216 -0
- package/templates/vault/whatsapp-cloud-api/assets/boilerplate/nodejs/src/webhook-handler.ts +173 -0
- package/templates/vault/whatsapp-cloud-api/assets/boilerplate/nodejs/src/whatsapp-client.ts +193 -0
- package/templates/vault/whatsapp-cloud-api/scripts/send_test_message.py +137 -0
- package/templates/vault/whatsapp-cloud-api/scripts/setup_project.py +118 -0
- package/templates/vault/whatsapp-cloud-api/scripts/validate_config.py +190 -0
- package/templates/vault/youtube-summarizer/scripts/extract-transcript.py +65 -0
- package/templates/vault/youtube-summarizer/scripts/install-dependencies.sh +28 -0
|
@@ -0,0 +1,1008 @@
|
|
|
1
|
+
"""007 Secrets Scanner -- Deep scanner for secrets and credentials.
|
|
2
|
+
|
|
3
|
+
Goes deeper than quick_scan by performing entropy analysis, base64 detection,
|
|
4
|
+
context-aware false positive reduction, and targeted scanning of sensitive
|
|
5
|
+
file types (.env, config files, shell scripts, Docker, CI/CD).
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python secrets_scanner.py --target /path/to/project
|
|
9
|
+
python secrets_scanner.py --target /path/to/project --output json --verbose
|
|
10
|
+
python secrets_scanner.py --target /path/to/project --include-low
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import base64
|
|
15
|
+
import json
|
|
16
|
+
import math
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Import from the 007 config hub (parent directory)
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
27
|
+
|
|
28
|
+
import config # noqa: E402
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Logger
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
logger = config.setup_logging("007-secrets-scanner")
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Additional patterns beyond config.SECRET_PATTERNS
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Each entry: (pattern_name, compiled_regex, severity)
|
|
39
|
+
|
|
40
|
+
_EXTRA_PATTERN_DEFS = [
|
|
41
|
+
# URLs with embedded credentials (http://user:pass@host)
|
|
42
|
+
(
|
|
43
|
+
"url_embedded_credentials",
|
|
44
|
+
r"""https?://[^:\s]+:[^@\s]+@[^\s/]+""",
|
|
45
|
+
"HIGH",
|
|
46
|
+
),
|
|
47
|
+
# Stripe keys
|
|
48
|
+
(
|
|
49
|
+
"stripe_key",
|
|
50
|
+
r"""(?:sk|pk)_(?:live|test)_[A-Za-z0-9]{20,}""",
|
|
51
|
+
"CRITICAL",
|
|
52
|
+
),
|
|
53
|
+
# Google API key
|
|
54
|
+
(
|
|
55
|
+
"google_api_key",
|
|
56
|
+
r"""AIza[0-9A-Za-z\-_]{35}""",
|
|
57
|
+
"HIGH",
|
|
58
|
+
),
|
|
59
|
+
# Twilio Account SID / Auth Token
|
|
60
|
+
(
|
|
61
|
+
"twilio_key",
|
|
62
|
+
r"""(?:AC[a-f0-9]{32}|SK[a-f0-9]{32})""",
|
|
63
|
+
"HIGH",
|
|
64
|
+
),
|
|
65
|
+
# Heroku API key
|
|
66
|
+
(
|
|
67
|
+
"heroku_api_key",
|
|
68
|
+
r"""(?i)heroku[_-]?api[_-]?key\s*[:=]\s*['\"]\S{8,}['\"]""",
|
|
69
|
+
"HIGH",
|
|
70
|
+
),
|
|
71
|
+
# SendGrid API key
|
|
72
|
+
(
|
|
73
|
+
"sendgrid_key",
|
|
74
|
+
r"""SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}""",
|
|
75
|
+
"CRITICAL",
|
|
76
|
+
),
|
|
77
|
+
# npm token
|
|
78
|
+
(
|
|
79
|
+
"npm_token",
|
|
80
|
+
r"""(?:npm_)[A-Za-z0-9]{36}""",
|
|
81
|
+
"CRITICAL",
|
|
82
|
+
),
|
|
83
|
+
# Generic connection string (ODBC / ADO style)
|
|
84
|
+
(
|
|
85
|
+
"connection_string",
|
|
86
|
+
r"""(?i)(?:connectionstring|conn_str)\s*[:=]\s*['\"][^'\"]{10,}['\"]""",
|
|
87
|
+
"HIGH",
|
|
88
|
+
),
|
|
89
|
+
# JWT tokens (three base64 segments separated by dots)
|
|
90
|
+
(
|
|
91
|
+
"jwt_token",
|
|
92
|
+
r"""eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}""",
|
|
93
|
+
"MEDIUM",
|
|
94
|
+
),
|
|
95
|
+
# Azure storage key
|
|
96
|
+
(
|
|
97
|
+
"azure_storage_key",
|
|
98
|
+
r"""(?i)(?:accountkey|storage[_-]?key)\s*[:=]\s*['\"]\S{44,}['\"]""",
|
|
99
|
+
"CRITICAL",
|
|
100
|
+
),
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
EXTRA_PATTERNS = [
|
|
104
|
+
(name, re.compile(pattern), severity)
|
|
105
|
+
for name, pattern, severity in _EXTRA_PATTERN_DEFS
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
# Combined pattern set: config patterns first, then extras
|
|
109
|
+
ALL_SECRET_PATTERNS = list(config.SECRET_PATTERNS) + EXTRA_PATTERNS
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
# Targeted file categories for deep scanning
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
# .env variants -- always scanned regardless of SCANNABLE_EXTENSIONS
|
|
117
|
+
ENV_FILE_PATTERNS = {
|
|
118
|
+
".env", ".env.local", ".env.production", ".env.staging",
|
|
119
|
+
".env.development", ".env.test", ".env.example", ".env.sample",
|
|
120
|
+
".env.defaults", ".env.template",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
CONFIG_EXTENSIONS = {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf"}
|
|
124
|
+
|
|
125
|
+
SHELL_EXTENSIONS = {".sh", ".bash", ".zsh", ".ps1", ".bat", ".cmd"}
|
|
126
|
+
|
|
127
|
+
DOCKER_PREFIXES = ("Dockerfile", "dockerfile", "docker-compose")
|
|
128
|
+
|
|
129
|
+
CICD_PATTERNS = {
|
|
130
|
+
".github/workflows",
|
|
131
|
+
".gitlab-ci.yml",
|
|
132
|
+
"Jenkinsfile",
|
|
133
|
+
".circleci/config.yml",
|
|
134
|
+
".travis.yml",
|
|
135
|
+
"azure-pipelines.yml",
|
|
136
|
+
"bitbucket-pipelines.yml",
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
PRIVATE_KEY_EXTENSIONS = {".pem", ".key", ".p12", ".pfx", ".jks", ".keystore"}
|
|
140
|
+
|
|
141
|
+
# Files that are test fixtures -- lower severity or skip
|
|
142
|
+
_TEST_FILE_PATTERNS = re.compile(
|
|
143
|
+
r"""(?i)(?:^test_|_test\.py$|\.test\.[jt]sx?$|\.spec\.[jt]sx?$|__tests__|fixtures?[/\\])"""
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Placeholder / example value patterns -- these are NOT real secrets
|
|
147
|
+
_PLACEHOLDER_PATTERN = re.compile(
|
|
148
|
+
r"""(?i)(?:example|placeholder|changeme|xxx+|your[_-]?key[_-]?here|"""
|
|
149
|
+
r"""insert[_-]?here|replace[_-]?me|todo|fixme|dummy|fake|sample|test123|"""
|
|
150
|
+
r"""sk_test_|pk_test_)"""
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# Entropy calculation
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
def shannon_entropy(s: str) -> float:
|
|
159
|
+
"""Calculate Shannon entropy of a string.
|
|
160
|
+
|
|
161
|
+
Higher entropy indicates more randomness, which may suggest a secret/token.
|
|
162
|
+
Typical English text: ~3.5-4.0 bits. Random tokens: ~4.5-6.0 bits.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
s: Input string.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Shannon entropy in bits. Returns 0.0 for empty strings.
|
|
169
|
+
"""
|
|
170
|
+
if not s:
|
|
171
|
+
return 0.0
|
|
172
|
+
|
|
173
|
+
length = len(s)
|
|
174
|
+
freq: dict[str, int] = {}
|
|
175
|
+
for ch in s:
|
|
176
|
+
freq[ch] = freq.get(ch, 0) + 1
|
|
177
|
+
|
|
178
|
+
entropy = 0.0
|
|
179
|
+
for count in freq.values():
|
|
180
|
+
probability = count / length
|
|
181
|
+
if probability > 0:
|
|
182
|
+
entropy -= probability * math.log2(probability)
|
|
183
|
+
|
|
184
|
+
return entropy
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
# Base64 detection
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
_BASE64_RE = re.compile(
|
|
192
|
+
r"""[A-Za-z0-9+/]{20,}={0,2}"""
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
_BASE64_URL_RE = re.compile(
|
|
196
|
+
r"""[A-Za-z0-9_-]{20,}"""
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _check_base64_secret(token: str) -> bool:
|
|
201
|
+
"""Check if a base64-looking string decodes to something high-entropy.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
token: A candidate base64 string.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
True if the decoded content has high entropy (likely a secret).
|
|
208
|
+
"""
|
|
209
|
+
# Pad if needed for standard base64
|
|
210
|
+
padded = token + "=" * (-len(token) % 4)
|
|
211
|
+
try:
|
|
212
|
+
decoded = base64.b64decode(padded, validate=True)
|
|
213
|
+
decoded_str = decoded.decode("ascii", errors="replace")
|
|
214
|
+
# Only flag if decoded content is also high entropy
|
|
215
|
+
return shannon_entropy(decoded_str) > 4.0 and len(decoded) >= 12
|
|
216
|
+
except Exception:
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
# Hardcoded IP detection
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
_IP_RE = re.compile(
|
|
225
|
+
r"""\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b"""
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
_SAFE_IP_PREFIXES = (
|
|
229
|
+
"127.", # localhost
|
|
230
|
+
"0.", # unspecified
|
|
231
|
+
"10.", # private class A
|
|
232
|
+
"192.168.", # private class C
|
|
233
|
+
"169.254.", # link-local
|
|
234
|
+
"255.", # broadcast
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _is_private_or_localhost(ip: str) -> bool:
|
|
239
|
+
"""Return True if IP is localhost, private range, or otherwise safe."""
|
|
240
|
+
if ip.startswith(_SAFE_IP_PREFIXES):
|
|
241
|
+
return True
|
|
242
|
+
# 172.16.0.0 - 172.31.255.255 (private class B)
|
|
243
|
+
parts = ip.split(".")
|
|
244
|
+
try:
|
|
245
|
+
if parts[0] == "172" and 16 <= int(parts[1]) <= 31:
|
|
246
|
+
return True
|
|
247
|
+
except (IndexError, ValueError):
|
|
248
|
+
pass
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
# Context-aware false positive reduction
|
|
254
|
+
# ---------------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
_COMMENT_LINE_RE = re.compile(
|
|
257
|
+
r"""^\s*(?:#|//|/\*|\*|;|rem\b|@rem\b)""", re.IGNORECASE
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
_MARKDOWN_CODE_FENCE = re.compile(r"""^\s*```""")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _is_comment_line(line: str) -> bool:
|
|
264
|
+
"""Return True if the line appears to be a comment."""
|
|
265
|
+
return bool(_COMMENT_LINE_RE.match(line))
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _is_test_file(filepath: Path) -> bool:
|
|
269
|
+
"""Return True if the file is a test fixture / test file."""
|
|
270
|
+
return bool(_TEST_FILE_PATTERNS.search(filepath.name)) or bool(
|
|
271
|
+
_TEST_FILE_PATTERNS.search(str(filepath))
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _is_placeholder_value(line: str) -> bool:
|
|
276
|
+
"""Return True if the matched line contains placeholder/example values."""
|
|
277
|
+
return bool(_PLACEHOLDER_PATTERN.search(line))
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _is_env_example(filepath: Path) -> bool:
|
|
281
|
+
"""Return True if the file is a .env.example or similar template."""
|
|
282
|
+
name = filepath.name.lower()
|
|
283
|
+
return name in (".env.example", ".env.sample", ".env.template", ".env.defaults")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _classify_file(filepath: Path) -> str:
|
|
287
|
+
"""Classify a file into a category for reporting.
|
|
288
|
+
|
|
289
|
+
Returns one of: 'env', 'config', 'shell', 'docker', 'cicd',
|
|
290
|
+
'private_key', 'source', 'other'.
|
|
291
|
+
"""
|
|
292
|
+
name = filepath.name.lower()
|
|
293
|
+
suffix = filepath.suffix.lower()
|
|
294
|
+
|
|
295
|
+
# .env variants
|
|
296
|
+
if name.startswith(".env") or name in ENV_FILE_PATTERNS:
|
|
297
|
+
return "env"
|
|
298
|
+
|
|
299
|
+
# Private key files
|
|
300
|
+
if suffix in PRIVATE_KEY_EXTENSIONS:
|
|
301
|
+
return "private_key"
|
|
302
|
+
|
|
303
|
+
# Config files
|
|
304
|
+
if suffix in CONFIG_EXTENSIONS:
|
|
305
|
+
return "config"
|
|
306
|
+
|
|
307
|
+
# Shell scripts
|
|
308
|
+
if suffix in SHELL_EXTENSIONS:
|
|
309
|
+
return "shell"
|
|
310
|
+
|
|
311
|
+
# Docker files
|
|
312
|
+
if any(name.startswith(prefix) for prefix in DOCKER_PREFIXES):
|
|
313
|
+
return "docker"
|
|
314
|
+
|
|
315
|
+
# CI/CD files
|
|
316
|
+
filepath_str = str(filepath).replace("\\", "/")
|
|
317
|
+
for cicd_pattern in CICD_PATTERNS:
|
|
318
|
+
if cicd_pattern in filepath_str:
|
|
319
|
+
return "cicd"
|
|
320
|
+
|
|
321
|
+
# Source code
|
|
322
|
+
if suffix in config.SCANNABLE_EXTENSIONS:
|
|
323
|
+
return "source"
|
|
324
|
+
|
|
325
|
+
return "other"
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# ---------------------------------------------------------------------------
|
|
329
|
+
# File collection (deeper than quick_scan)
|
|
330
|
+
# ---------------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
def _should_scan_file(filepath: Path) -> bool:
|
|
333
|
+
"""Determine if a file should be included in the deep scan.
|
|
334
|
+
|
|
335
|
+
More inclusive than quick_scan: also picks up .env variants, Docker files,
|
|
336
|
+
CI/CD files, and private key files even if their extension is not in
|
|
337
|
+
SCANNABLE_EXTENSIONS.
|
|
338
|
+
"""
|
|
339
|
+
name = filepath.name.lower()
|
|
340
|
+
suffix = filepath.suffix.lower()
|
|
341
|
+
|
|
342
|
+
# Always scan .env variants
|
|
343
|
+
if name.startswith(".env"):
|
|
344
|
+
return True
|
|
345
|
+
|
|
346
|
+
# Always scan private key files (we detect their presence, not content)
|
|
347
|
+
if suffix in PRIVATE_KEY_EXTENSIONS:
|
|
348
|
+
return True
|
|
349
|
+
|
|
350
|
+
# Always scan Docker files
|
|
351
|
+
if any(name.startswith(prefix) for prefix in DOCKER_PREFIXES):
|
|
352
|
+
return True
|
|
353
|
+
|
|
354
|
+
# Always scan CI/CD files
|
|
355
|
+
filepath_str = str(filepath).replace("\\", "/")
|
|
356
|
+
for cicd_pattern in CICD_PATTERNS:
|
|
357
|
+
if cicd_pattern in filepath_str or name == Path(cicd_pattern).name:
|
|
358
|
+
return True
|
|
359
|
+
|
|
360
|
+
# Standard scannable extensions
|
|
361
|
+
for ext in config.SCANNABLE_EXTENSIONS:
|
|
362
|
+
if name.endswith(ext):
|
|
363
|
+
return True
|
|
364
|
+
if suffix in config.SCANNABLE_EXTENSIONS:
|
|
365
|
+
return True
|
|
366
|
+
|
|
367
|
+
return False
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def collect_files(target: Path) -> list[Path]:
|
|
371
|
+
"""Walk *target* recursively and return files for deep scanning.
|
|
372
|
+
|
|
373
|
+
Respects SKIP_DIRECTORIES but is more inclusive on file types.
|
|
374
|
+
"""
|
|
375
|
+
files: list[Path] = []
|
|
376
|
+
max_files = config.LIMITS["max_files_per_scan"]
|
|
377
|
+
|
|
378
|
+
for root, dirs, filenames in os.walk(target):
|
|
379
|
+
dirs[:] = [d for d in dirs if d not in config.SKIP_DIRECTORIES]
|
|
380
|
+
|
|
381
|
+
for fname in filenames:
|
|
382
|
+
if len(files) >= max_files:
|
|
383
|
+
logger.warning(
|
|
384
|
+
"Reached max_files_per_scan limit (%d). Stopping.", max_files
|
|
385
|
+
)
|
|
386
|
+
return files
|
|
387
|
+
|
|
388
|
+
fpath = Path(root) / fname
|
|
389
|
+
if _should_scan_file(fpath):
|
|
390
|
+
files.append(fpath)
|
|
391
|
+
|
|
392
|
+
return files
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
# ---------------------------------------------------------------------------
|
|
396
|
+
# Core scanning logic
|
|
397
|
+
# ---------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
def _redact(text: str, keep: int = 6) -> str:
|
|
400
|
+
"""Return a redacted version of *text*, keeping only the first few chars."""
|
|
401
|
+
text = text.strip()
|
|
402
|
+
if len(text) <= keep:
|
|
403
|
+
return text
|
|
404
|
+
return text[:keep] + "****"
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _snippet(line: str, match_start: int, context: int = 50) -> str:
|
|
408
|
+
"""Extract a short redacted snippet around the match position."""
|
|
409
|
+
start = max(0, match_start - context // 2)
|
|
410
|
+
end = min(len(line), match_start + context)
|
|
411
|
+
raw = line[start:end].strip()
|
|
412
|
+
return _redact(raw)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def scan_file(filepath: Path, verbose: bool = False) -> list[dict]:
|
|
416
|
+
"""Perform deep secret scanning on a single file.
|
|
417
|
+
|
|
418
|
+
Applies pattern matching, entropy analysis, base64 detection,
|
|
419
|
+
URL credential detection, IP detection, and context-aware filtering.
|
|
420
|
+
|
|
421
|
+
Returns a list of finding dicts.
|
|
422
|
+
"""
|
|
423
|
+
findings: list[dict] = []
|
|
424
|
+
max_findings = config.LIMITS["max_findings_per_file"]
|
|
425
|
+
file_str = str(filepath)
|
|
426
|
+
file_category = _classify_file(filepath)
|
|
427
|
+
is_test = _is_test_file(filepath)
|
|
428
|
+
is_env_ex = _is_env_example(filepath)
|
|
429
|
+
|
|
430
|
+
# --- Private key file detection (by extension, not content) ---
|
|
431
|
+
if filepath.suffix.lower() in PRIVATE_KEY_EXTENSIONS:
|
|
432
|
+
sev = "MEDIUM" if is_test else "CRITICAL"
|
|
433
|
+
findings.append({
|
|
434
|
+
"type": "secret",
|
|
435
|
+
"pattern": "private_key_file",
|
|
436
|
+
"severity": sev,
|
|
437
|
+
"file": file_str,
|
|
438
|
+
"line": 0,
|
|
439
|
+
"snippet": f"Private key file detected: {filepath.name}",
|
|
440
|
+
"category": file_category,
|
|
441
|
+
})
|
|
442
|
+
# Still scan content if readable
|
|
443
|
+
# (fall through)
|
|
444
|
+
|
|
445
|
+
# --- File size check ---
|
|
446
|
+
try:
|
|
447
|
+
size = filepath.stat().st_size
|
|
448
|
+
except OSError:
|
|
449
|
+
return findings
|
|
450
|
+
|
|
451
|
+
if size > config.LIMITS["max_file_size_bytes"]:
|
|
452
|
+
if verbose:
|
|
453
|
+
logger.debug("Skipping oversized file: %s (%d bytes)", filepath, size)
|
|
454
|
+
return findings
|
|
455
|
+
|
|
456
|
+
# --- Read content ---
|
|
457
|
+
try:
|
|
458
|
+
text = filepath.read_text(encoding="utf-8", errors="replace")
|
|
459
|
+
except OSError as exc:
|
|
460
|
+
if verbose:
|
|
461
|
+
logger.debug("Cannot read %s: %s", filepath, exc)
|
|
462
|
+
return findings
|
|
463
|
+
|
|
464
|
+
lines = text.splitlines()
|
|
465
|
+
in_markdown_code_block = False
|
|
466
|
+
|
|
467
|
+
for line_num, line in enumerate(lines, start=1):
|
|
468
|
+
if len(findings) >= max_findings:
|
|
469
|
+
break
|
|
470
|
+
|
|
471
|
+
stripped = line.strip()
|
|
472
|
+
if not stripped:
|
|
473
|
+
continue
|
|
474
|
+
|
|
475
|
+
# Track markdown code fences for context-aware filtering
|
|
476
|
+
if _MARKDOWN_CODE_FENCE.match(stripped):
|
|
477
|
+
in_markdown_code_block = not in_markdown_code_block
|
|
478
|
+
continue
|
|
479
|
+
|
|
480
|
+
# Context-aware filters
|
|
481
|
+
is_comment = _is_comment_line(stripped)
|
|
482
|
+
is_placeholder = _is_placeholder_value(stripped)
|
|
483
|
+
|
|
484
|
+
# --- Pattern matching (config + extra patterns) ---
|
|
485
|
+
for pattern_name, regex, severity in ALL_SECRET_PATTERNS:
|
|
486
|
+
m = regex.search(line)
|
|
487
|
+
if not m:
|
|
488
|
+
continue
|
|
489
|
+
|
|
490
|
+
# Apply false positive reduction
|
|
491
|
+
skip = False
|
|
492
|
+
adjusted_severity = severity
|
|
493
|
+
|
|
494
|
+
if is_comment and not file_category == "env":
|
|
495
|
+
# Comments in source code are usually not real secrets
|
|
496
|
+
# But comments in .env files might still be sensitive
|
|
497
|
+
skip = True
|
|
498
|
+
|
|
499
|
+
if in_markdown_code_block:
|
|
500
|
+
skip = True
|
|
501
|
+
|
|
502
|
+
if is_placeholder:
|
|
503
|
+
skip = True
|
|
504
|
+
|
|
505
|
+
if is_test:
|
|
506
|
+
# Lower severity for test files
|
|
507
|
+
sev_weight = config.SEVERITY.get(severity, 1)
|
|
508
|
+
if sev_weight >= config.SEVERITY["HIGH"]:
|
|
509
|
+
adjusted_severity = "MEDIUM"
|
|
510
|
+
elif sev_weight >= config.SEVERITY["MEDIUM"]:
|
|
511
|
+
adjusted_severity = "LOW"
|
|
512
|
+
|
|
513
|
+
if is_env_ex:
|
|
514
|
+
# .env.example should have placeholders, not real values
|
|
515
|
+
# If pattern matches, it might be a real secret leaked into example
|
|
516
|
+
if not is_placeholder:
|
|
517
|
+
adjusted_severity = "MEDIUM" # flag but lower severity
|
|
518
|
+
else:
|
|
519
|
+
skip = True # placeholder in example = expected
|
|
520
|
+
|
|
521
|
+
if skip:
|
|
522
|
+
continue
|
|
523
|
+
|
|
524
|
+
findings.append({
|
|
525
|
+
"type": "secret",
|
|
526
|
+
"pattern": pattern_name,
|
|
527
|
+
"severity": adjusted_severity,
|
|
528
|
+
"file": file_str,
|
|
529
|
+
"line": line_num,
|
|
530
|
+
"snippet": _snippet(line, m.start()),
|
|
531
|
+
"category": file_category,
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
# --- High entropy string detection ---
|
|
535
|
+
# Look for quoted strings or assignment values 16+ chars
|
|
536
|
+
for token_match in re.finditer(r"""['"]([^'"]{16,})['\"]""", line):
|
|
537
|
+
if len(findings) >= max_findings:
|
|
538
|
+
break
|
|
539
|
+
|
|
540
|
+
token = token_match.group(1)
|
|
541
|
+
ent = shannon_entropy(token)
|
|
542
|
+
|
|
543
|
+
if ent > 4.5:
|
|
544
|
+
# Skip if already caught by pattern matching
|
|
545
|
+
# (crude check: see if any finding on this line already)
|
|
546
|
+
already_found = any(
|
|
547
|
+
f["file"] == file_str and f["line"] == line_num
|
|
548
|
+
for f in findings
|
|
549
|
+
)
|
|
550
|
+
if already_found:
|
|
551
|
+
continue
|
|
552
|
+
|
|
553
|
+
if is_comment or in_markdown_code_block or is_placeholder:
|
|
554
|
+
continue
|
|
555
|
+
|
|
556
|
+
sev = "MEDIUM"
|
|
557
|
+
if ent > 5.0:
|
|
558
|
+
sev = "HIGH"
|
|
559
|
+
if is_test:
|
|
560
|
+
sev = "LOW"
|
|
561
|
+
|
|
562
|
+
findings.append({
|
|
563
|
+
"type": "secret",
|
|
564
|
+
"pattern": "high_entropy_string",
|
|
565
|
+
"severity": sev,
|
|
566
|
+
"file": file_str,
|
|
567
|
+
"line": line_num,
|
|
568
|
+
"snippet": _redact(token),
|
|
569
|
+
"category": file_category,
|
|
570
|
+
"entropy": round(ent, 2),
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
# --- Base64-encoded secret detection ---
|
|
574
|
+
for b64_match in _BASE64_RE.finditer(line):
|
|
575
|
+
if len(findings) >= max_findings:
|
|
576
|
+
break
|
|
577
|
+
|
|
578
|
+
token = b64_match.group(0)
|
|
579
|
+
if len(token) < 20:
|
|
580
|
+
continue
|
|
581
|
+
|
|
582
|
+
# Skip if already caught
|
|
583
|
+
already_found = any(
|
|
584
|
+
f["file"] == file_str and f["line"] == line_num
|
|
585
|
+
for f in findings
|
|
586
|
+
)
|
|
587
|
+
if already_found:
|
|
588
|
+
continue
|
|
589
|
+
|
|
590
|
+
if is_comment or in_markdown_code_block or is_placeholder:
|
|
591
|
+
continue
|
|
592
|
+
|
|
593
|
+
if _check_base64_secret(token):
|
|
594
|
+
sev = "MEDIUM" if is_test else "HIGH"
|
|
595
|
+
findings.append({
|
|
596
|
+
"type": "secret",
|
|
597
|
+
"pattern": "base64_encoded_secret",
|
|
598
|
+
"severity": sev,
|
|
599
|
+
"file": file_str,
|
|
600
|
+
"line": line_num,
|
|
601
|
+
"snippet": _redact(token),
|
|
602
|
+
"category": file_category,
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
# --- URL with embedded credentials ---
|
|
606
|
+
# Already handled by pattern, but double-check for non-standard schemes
|
|
607
|
+
# (covered by url_embedded_credentials pattern)
|
|
608
|
+
|
|
609
|
+
# --- Hardcoded IP detection ---
|
|
610
|
+
for ip_match in _IP_RE.finditer(line):
|
|
611
|
+
if len(findings) >= max_findings:
|
|
612
|
+
break
|
|
613
|
+
|
|
614
|
+
ip = ip_match.group(1)
|
|
615
|
+
if _is_private_or_localhost(ip):
|
|
616
|
+
continue
|
|
617
|
+
|
|
618
|
+
# Validate it looks like a real IP (each octet 0-255)
|
|
619
|
+
parts = ip.split(".")
|
|
620
|
+
try:
|
|
621
|
+
if not all(0 <= int(p) <= 255 for p in parts):
|
|
622
|
+
continue
|
|
623
|
+
except ValueError:
|
|
624
|
+
continue
|
|
625
|
+
|
|
626
|
+
if is_comment or in_markdown_code_block:
|
|
627
|
+
continue
|
|
628
|
+
|
|
629
|
+
sev = "LOW"
|
|
630
|
+
if is_test:
|
|
631
|
+
continue # Skip IPs in test files entirely
|
|
632
|
+
|
|
633
|
+
findings.append({
|
|
634
|
+
"type": "hardcoded_ip",
|
|
635
|
+
"pattern": "hardcoded_public_ip",
|
|
636
|
+
"severity": sev,
|
|
637
|
+
"file": file_str,
|
|
638
|
+
"line": line_num,
|
|
639
|
+
"snippet": ip,
|
|
640
|
+
"category": file_category,
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
return findings
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
# ---------------------------------------------------------------------------
|
|
647
|
+
# Aggregation and scoring
|
|
648
|
+
# ---------------------------------------------------------------------------
|
|
649
|
+
|
|
650
|
+
SCORE_DEDUCTIONS = {
|
|
651
|
+
"CRITICAL": 10,
|
|
652
|
+
"HIGH": 5,
|
|
653
|
+
"MEDIUM": 2,
|
|
654
|
+
"LOW": 1,
|
|
655
|
+
"INFO": 0,
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def aggregate_by_severity(findings: list[dict]) -> dict[str, int]:
|
|
660
|
+
"""Count findings per severity level."""
|
|
661
|
+
counts: dict[str, int] = {sev: 0 for sev in config.SEVERITY}
|
|
662
|
+
for f in findings:
|
|
663
|
+
sev = f.get("severity", "INFO")
|
|
664
|
+
if sev in counts:
|
|
665
|
+
counts[sev] += 1
|
|
666
|
+
return counts
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def aggregate_by_pattern(findings: list[dict]) -> dict[str, int]:
|
|
670
|
+
"""Count findings per pattern type."""
|
|
671
|
+
counts: dict[str, int] = {}
|
|
672
|
+
for f in findings:
|
|
673
|
+
pattern = f.get("pattern", "unknown")
|
|
674
|
+
counts[pattern] = counts.get(pattern, 0) + 1
|
|
675
|
+
return counts
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def aggregate_by_category(findings: list[dict]) -> dict[str, int]:
|
|
679
|
+
"""Count findings per file category."""
|
|
680
|
+
counts: dict[str, int] = {}
|
|
681
|
+
for f in findings:
|
|
682
|
+
cat = f.get("category", "other")
|
|
683
|
+
counts[cat] = counts.get(cat, 0) + 1
|
|
684
|
+
return counts
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def compute_score(findings: list[dict]) -> int:
|
|
688
|
+
"""Compute a secrets score starting at 100, deducting by severity."""
|
|
689
|
+
score = 100
|
|
690
|
+
for f in findings:
|
|
691
|
+
deduction = SCORE_DEDUCTIONS.get(f["severity"], 0)
|
|
692
|
+
score -= deduction
|
|
693
|
+
return max(0, score)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
# ---------------------------------------------------------------------------
|
|
697
|
+
# Report formatters
|
|
698
|
+
# ---------------------------------------------------------------------------
|
|
699
|
+
|
|
700
|
+
def format_text_report(
|
|
701
|
+
target: str,
|
|
702
|
+
total_files: int,
|
|
703
|
+
findings: list[dict],
|
|
704
|
+
severity_counts: dict[str, int],
|
|
705
|
+
pattern_counts: dict[str, int],
|
|
706
|
+
category_counts: dict[str, int],
|
|
707
|
+
score: int,
|
|
708
|
+
verdict: dict,
|
|
709
|
+
elapsed: float,
|
|
710
|
+
include_low: bool = False,
|
|
711
|
+
) -> str:
|
|
712
|
+
"""Build a human-readable text report grouped by severity, then file."""
|
|
713
|
+
lines: list[str] = []
|
|
714
|
+
|
|
715
|
+
lines.append("=" * 72)
|
|
716
|
+
lines.append(" 007 SECRETS SCANNER -- DEEP SCAN REPORT")
|
|
717
|
+
lines.append("=" * 72)
|
|
718
|
+
lines.append("")
|
|
719
|
+
|
|
720
|
+
# Metadata
|
|
721
|
+
lines.append(f" Target: {target}")
|
|
722
|
+
lines.append(f" Timestamp: {config.get_timestamp()}")
|
|
723
|
+
lines.append(f" Duration: {elapsed:.2f}s")
|
|
724
|
+
lines.append(f" Files scanned: {total_files}")
|
|
725
|
+
lines.append(f" Total findings: {len(findings)}")
|
|
726
|
+
lines.append("")
|
|
727
|
+
|
|
728
|
+
# Severity breakdown
|
|
729
|
+
lines.append("-" * 72)
|
|
730
|
+
lines.append(" FINDINGS BY SEVERITY")
|
|
731
|
+
lines.append("-" * 72)
|
|
732
|
+
for sev in ("CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"):
|
|
733
|
+
count = severity_counts.get(sev, 0)
|
|
734
|
+
bar = "#" * min(count, 40)
|
|
735
|
+
lines.append(f" {sev:<10} {count:>5} {bar}")
|
|
736
|
+
lines.append("")
|
|
737
|
+
|
|
738
|
+
# Pattern type breakdown
|
|
739
|
+
if pattern_counts:
|
|
740
|
+
lines.append("-" * 72)
|
|
741
|
+
lines.append(" FINDINGS BY TYPE")
|
|
742
|
+
lines.append("-" * 72)
|
|
743
|
+
sorted_patterns = sorted(pattern_counts.items(), key=lambda x: x[1], reverse=True)
|
|
744
|
+
for pattern_name, count in sorted_patterns[:20]:
|
|
745
|
+
lines.append(f" {pattern_name:<35} {count:>5}")
|
|
746
|
+
lines.append("")
|
|
747
|
+
|
|
748
|
+
# Category breakdown
|
|
749
|
+
if category_counts:
|
|
750
|
+
lines.append("-" * 72)
|
|
751
|
+
lines.append(" FINDINGS BY FILE CATEGORY")
|
|
752
|
+
lines.append("-" * 72)
|
|
753
|
+
sorted_cats = sorted(category_counts.items(), key=lambda x: x[1], reverse=True)
|
|
754
|
+
for cat_name, count in sorted_cats:
|
|
755
|
+
lines.append(f" {cat_name:<20} {count:>5}")
|
|
756
|
+
lines.append("")
|
|
757
|
+
|
|
758
|
+
# Findings grouped by severity, then by file
|
|
759
|
+
min_severity = config.SEVERITY["LOW"] if include_low else config.SEVERITY["MEDIUM"]
|
|
760
|
+
|
|
761
|
+
displayed = [
|
|
762
|
+
f for f in findings
|
|
763
|
+
if config.SEVERITY.get(f.get("severity", "INFO"), 0) >= min_severity
|
|
764
|
+
]
|
|
765
|
+
|
|
766
|
+
if displayed:
|
|
767
|
+
# Group by severity
|
|
768
|
+
by_severity: dict[str, list[dict]] = {}
|
|
769
|
+
for f in displayed:
|
|
770
|
+
sev = f.get("severity", "INFO")
|
|
771
|
+
by_severity.setdefault(sev, []).append(f)
|
|
772
|
+
|
|
773
|
+
for sev in ("CRITICAL", "HIGH", "MEDIUM", "LOW"):
|
|
774
|
+
sev_findings = by_severity.get(sev, [])
|
|
775
|
+
if not sev_findings:
|
|
776
|
+
continue
|
|
777
|
+
|
|
778
|
+
lines.append("-" * 72)
|
|
779
|
+
lines.append(f" [{sev}] FINDINGS ({len(sev_findings)})")
|
|
780
|
+
lines.append("-" * 72)
|
|
781
|
+
|
|
782
|
+
# Sub-group by file
|
|
783
|
+
by_file: dict[str, list[dict]] = {}
|
|
784
|
+
for f in sev_findings:
|
|
785
|
+
by_file.setdefault(f["file"], []).append(f)
|
|
786
|
+
|
|
787
|
+
for filepath, file_findings in sorted(by_file.items()):
|
|
788
|
+
lines.append(f" {filepath}")
|
|
789
|
+
for f in sorted(file_findings, key=lambda x: x.get("line", 0)):
|
|
790
|
+
loc = f"L{f['line']}" if f.get("line") else ""
|
|
791
|
+
snippet_part = f" [{f['snippet']}]" if f.get("snippet") else ""
|
|
792
|
+
entropy_part = f" (entropy={f['entropy']})" if f.get("entropy") else ""
|
|
793
|
+
lines.append(
|
|
794
|
+
f" {loc:>6} {f['pattern']}{snippet_part}{entropy_part}"
|
|
795
|
+
)
|
|
796
|
+
lines.append("")
|
|
797
|
+
else:
|
|
798
|
+
lines.append(" No findings above the display threshold.")
|
|
799
|
+
lines.append("")
|
|
800
|
+
|
|
801
|
+
# Score and verdict
|
|
802
|
+
lines.append("=" * 72)
|
|
803
|
+
lines.append(f" SECRETS SCORE: {score} / 100")
|
|
804
|
+
lines.append(f" VERDICT: {verdict['emoji']} {verdict['label']}")
|
|
805
|
+
lines.append(f" {verdict['description']}")
|
|
806
|
+
lines.append("=" * 72)
|
|
807
|
+
lines.append("")
|
|
808
|
+
|
|
809
|
+
return "\n".join(lines)
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def build_json_report(
|
|
813
|
+
target: str,
|
|
814
|
+
total_files: int,
|
|
815
|
+
findings: list[dict],
|
|
816
|
+
severity_counts: dict[str, int],
|
|
817
|
+
pattern_counts: dict[str, int],
|
|
818
|
+
category_counts: dict[str, int],
|
|
819
|
+
score: int,
|
|
820
|
+
verdict: dict,
|
|
821
|
+
elapsed: float,
|
|
822
|
+
) -> dict:
|
|
823
|
+
"""Build a structured JSON-serializable report dict."""
|
|
824
|
+
return {
|
|
825
|
+
"scan": "secrets_scanner",
|
|
826
|
+
"target": target,
|
|
827
|
+
"timestamp": config.get_timestamp(),
|
|
828
|
+
"duration_seconds": round(elapsed, 3),
|
|
829
|
+
"total_files_scanned": total_files,
|
|
830
|
+
"total_findings": len(findings),
|
|
831
|
+
"severity_counts": severity_counts,
|
|
832
|
+
"pattern_counts": pattern_counts,
|
|
833
|
+
"category_counts": category_counts,
|
|
834
|
+
"score": score,
|
|
835
|
+
"verdict": {
|
|
836
|
+
"label": verdict["label"],
|
|
837
|
+
"description": verdict["description"],
|
|
838
|
+
"emoji": verdict["emoji"],
|
|
839
|
+
},
|
|
840
|
+
"findings": findings,
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
# ---------------------------------------------------------------------------
|
|
845
|
+
# Main entry point
|
|
846
|
+
# ---------------------------------------------------------------------------
|
|
847
|
+
|
|
848
|
+
def run_scan(
|
|
849
|
+
target_path: str,
|
|
850
|
+
output_format: str = "text",
|
|
851
|
+
verbose: bool = False,
|
|
852
|
+
include_low: bool = False,
|
|
853
|
+
) -> dict:
|
|
854
|
+
"""Execute the deep secrets scan and return the report dict.
|
|
855
|
+
|
|
856
|
+
Also prints the report to stdout in the requested format.
|
|
857
|
+
|
|
858
|
+
Args:
|
|
859
|
+
target_path: Path to the directory to scan.
|
|
860
|
+
output_format: 'text' or 'json'.
|
|
861
|
+
verbose: Enable debug-level logging.
|
|
862
|
+
include_low: Include LOW severity findings in text output.
|
|
863
|
+
|
|
864
|
+
Returns:
|
|
865
|
+
JSON-compatible report dict.
|
|
866
|
+
"""
|
|
867
|
+
if verbose:
|
|
868
|
+
logger.setLevel("DEBUG")
|
|
869
|
+
|
|
870
|
+
config.ensure_directories()
|
|
871
|
+
|
|
872
|
+
target = Path(target_path).resolve()
|
|
873
|
+
if not target.exists():
|
|
874
|
+
logger.error("Target path does not exist: %s", target)
|
|
875
|
+
sys.exit(1)
|
|
876
|
+
if not target.is_dir():
|
|
877
|
+
logger.error("Target is not a directory: %s", target)
|
|
878
|
+
sys.exit(1)
|
|
879
|
+
|
|
880
|
+
logger.info("Starting deep secrets scan of %s", target)
|
|
881
|
+
start_time = time.time()
|
|
882
|
+
|
|
883
|
+
# Collect files
|
|
884
|
+
files = collect_files(target)
|
|
885
|
+
total_files = len(files)
|
|
886
|
+
logger.info("Collected %d files for deep scanning", total_files)
|
|
887
|
+
|
|
888
|
+
# Scan each file
|
|
889
|
+
all_findings: list[dict] = []
|
|
890
|
+
max_report = config.LIMITS["max_report_findings"]
|
|
891
|
+
|
|
892
|
+
for fpath in files:
|
|
893
|
+
if len(all_findings) >= max_report:
|
|
894
|
+
logger.warning(
|
|
895
|
+
"Reached max_report_findings limit (%d). Truncating.", max_report
|
|
896
|
+
)
|
|
897
|
+
break
|
|
898
|
+
|
|
899
|
+
file_findings = scan_file(fpath, verbose=verbose)
|
|
900
|
+
remaining = max_report - len(all_findings)
|
|
901
|
+
all_findings.extend(file_findings[:remaining])
|
|
902
|
+
|
|
903
|
+
elapsed = time.time() - start_time
|
|
904
|
+
logger.info(
|
|
905
|
+
"Deep scan complete: %d files, %d findings in %.2fs",
|
|
906
|
+
total_files, len(all_findings), elapsed,
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
# Aggregation
|
|
910
|
+
severity_counts = aggregate_by_severity(all_findings)
|
|
911
|
+
pattern_counts = aggregate_by_pattern(all_findings)
|
|
912
|
+
category_counts = aggregate_by_category(all_findings)
|
|
913
|
+
score = compute_score(all_findings)
|
|
914
|
+
verdict = config.get_verdict(score)
|
|
915
|
+
|
|
916
|
+
# Audit log
|
|
917
|
+
config.log_audit_event(
|
|
918
|
+
action="secrets_scan",
|
|
919
|
+
target=str(target),
|
|
920
|
+
result=f"score={score}, findings={len(all_findings)}, verdict={verdict['label']}",
|
|
921
|
+
details={
|
|
922
|
+
"total_files": total_files,
|
|
923
|
+
"severity_counts": severity_counts,
|
|
924
|
+
"pattern_counts": pattern_counts,
|
|
925
|
+
"category_counts": category_counts,
|
|
926
|
+
"duration_seconds": round(elapsed, 3),
|
|
927
|
+
},
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
# Build report
|
|
931
|
+
report = build_json_report(
|
|
932
|
+
target=str(target),
|
|
933
|
+
total_files=total_files,
|
|
934
|
+
findings=all_findings,
|
|
935
|
+
severity_counts=severity_counts,
|
|
936
|
+
pattern_counts=pattern_counts,
|
|
937
|
+
category_counts=category_counts,
|
|
938
|
+
score=score,
|
|
939
|
+
verdict=verdict,
|
|
940
|
+
elapsed=elapsed,
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
# Output
|
|
944
|
+
if output_format == "json":
|
|
945
|
+
print(json.dumps(report, indent=2, ensure_ascii=False))
|
|
946
|
+
else:
|
|
947
|
+
print(format_text_report(
|
|
948
|
+
target=str(target),
|
|
949
|
+
total_files=total_files,
|
|
950
|
+
findings=all_findings,
|
|
951
|
+
severity_counts=severity_counts,
|
|
952
|
+
pattern_counts=pattern_counts,
|
|
953
|
+
category_counts=category_counts,
|
|
954
|
+
score=score,
|
|
955
|
+
verdict=verdict,
|
|
956
|
+
elapsed=elapsed,
|
|
957
|
+
include_low=include_low,
|
|
958
|
+
))
|
|
959
|
+
|
|
960
|
+
return report
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
# ---------------------------------------------------------------------------
|
|
964
|
+
# CLI
|
|
965
|
+
# ---------------------------------------------------------------------------
|
|
966
|
+
|
|
967
|
+
if __name__ == "__main__":
|
|
968
|
+
parser = argparse.ArgumentParser(
|
|
969
|
+
description="007 Secrets Scanner -- Deep scanner for secrets and credentials.",
|
|
970
|
+
epilog=(
|
|
971
|
+
"Examples:\n"
|
|
972
|
+
" python secrets_scanner.py --target ./my-project\n"
|
|
973
|
+
" python secrets_scanner.py --target ./my-project --output json\n"
|
|
974
|
+
" python secrets_scanner.py --target ./my-project --verbose --include-low"
|
|
975
|
+
),
|
|
976
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
977
|
+
)
|
|
978
|
+
parser.add_argument(
|
|
979
|
+
"--target",
|
|
980
|
+
required=True,
|
|
981
|
+
help="Path to the directory to scan (required).",
|
|
982
|
+
)
|
|
983
|
+
parser.add_argument(
|
|
984
|
+
"--output",
|
|
985
|
+
choices=["text", "json"],
|
|
986
|
+
default="text",
|
|
987
|
+
help="Output format: 'text' (default) or 'json'.",
|
|
988
|
+
)
|
|
989
|
+
parser.add_argument(
|
|
990
|
+
"--verbose",
|
|
991
|
+
action="store_true",
|
|
992
|
+
default=False,
|
|
993
|
+
help="Enable verbose/debug logging.",
|
|
994
|
+
)
|
|
995
|
+
parser.add_argument(
|
|
996
|
+
"--include-low",
|
|
997
|
+
action="store_true",
|
|
998
|
+
default=False,
|
|
999
|
+
help="Include LOW severity findings in text output (hidden by default).",
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
args = parser.parse_args()
|
|
1003
|
+
run_scan(
|
|
1004
|
+
target_path=args.target,
|
|
1005
|
+
output_format=args.output,
|
|
1006
|
+
verbose=args.verbose,
|
|
1007
|
+
include_low=args.include_low,
|
|
1008
|
+
)
|