malwar 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- malwar-0.2.1/.dockerignore +19 -0
- malwar-0.2.1/.env.example +27 -0
- malwar-0.2.1/.github/FUNDING.yml +1 -0
- malwar-0.2.1/.github/ISSUE_TEMPLATE/bug_report.yml +53 -0
- malwar-0.2.1/.github/ISSUE_TEMPLATE/detection_rule.yml +55 -0
- malwar-0.2.1/.github/ISSUE_TEMPLATE/feature_request.yml +26 -0
- malwar-0.2.1/.github/PULL_REQUEST_TEMPLATE.md +19 -0
- malwar-0.2.1/.github/actions/scan-skills/action.yml +115 -0
- malwar-0.2.1/.github/actions/scan-skills/scan.py +270 -0
- malwar-0.2.1/.github/workflows/ci.yml +134 -0
- malwar-0.2.1/.github/workflows/docker.yml +61 -0
- malwar-0.2.1/.github/workflows/docs.yml +34 -0
- malwar-0.2.1/.github/workflows/publish-pypi.yml +87 -0
- malwar-0.2.1/.github/workflows/scan-skills.yml +40 -0
- malwar-0.2.1/.gitignore +36 -0
- malwar-0.2.1/CONTRIBUTING.md +70 -0
- malwar-0.2.1/Dockerfile +43 -0
- malwar-0.2.1/LICENSE +68 -0
- malwar-0.2.1/PKG-INFO +209 -0
- malwar-0.2.1/README.md +161 -0
- malwar-0.2.1/SECURITY.md +38 -0
- malwar-0.2.1/deploy/helm/malwar/.helmignore +19 -0
- malwar-0.2.1/deploy/helm/malwar/Chart.yaml +17 -0
- malwar-0.2.1/deploy/helm/malwar/templates/NOTES.txt +38 -0
- malwar-0.2.1/deploy/helm/malwar/templates/_helpers.tpl +67 -0
- malwar-0.2.1/deploy/helm/malwar/templates/configmap.yaml +11 -0
- malwar-0.2.1/deploy/helm/malwar/templates/deployment.yaml +95 -0
- malwar-0.2.1/deploy/helm/malwar/templates/hpa.yaml +33 -0
- malwar-0.2.1/deploy/helm/malwar/templates/ingress.yaml +42 -0
- malwar-0.2.1/deploy/helm/malwar/templates/pvc.yaml +24 -0
- malwar-0.2.1/deploy/helm/malwar/templates/secret.yaml +21 -0
- malwar-0.2.1/deploy/helm/malwar/templates/service.yaml +16 -0
- malwar-0.2.1/deploy/helm/malwar/templates/serviceaccount.yaml +14 -0
- malwar-0.2.1/deploy/helm/malwar/templates/tests/test-connection.yaml +16 -0
- malwar-0.2.1/deploy/helm/malwar/values.yaml +101 -0
- malwar-0.2.1/docker-compose.yml +14 -0
- malwar-0.2.1/docs/accuracy-report.md +199 -0
- malwar-0.2.1/docs/api-reference.md +778 -0
- malwar-0.2.1/docs/architecture.md +386 -0
- malwar-0.2.1/docs/benchmarks.md +223 -0
- malwar-0.2.1/docs/campaign-ingestion.md +226 -0
- malwar-0.2.1/docs/cli-reference.md +449 -0
- malwar-0.2.1/docs/deployment/configuration.md +190 -0
- malwar-0.2.1/docs/deployment/docker.md +176 -0
- malwar-0.2.1/docs/deployment/kubernetes.md +212 -0
- malwar-0.2.1/docs/deployment.md +394 -0
- malwar-0.2.1/docs/detection-rules.md +618 -0
- malwar-0.2.1/docs/development/architecture.md +192 -0
- malwar-0.2.1/docs/development/benchmarks.md +83 -0
- malwar-0.2.1/docs/development/contributing.md +216 -0
- malwar-0.2.1/docs/development.md +558 -0
- malwar-0.2.1/docs/getting-started/installation.md +146 -0
- malwar-0.2.1/docs/getting-started/quickstart.md +181 -0
- malwar-0.2.1/docs/github-action.md +197 -0
- malwar-0.2.1/docs/guide/accuracy-report.md +123 -0
- malwar-0.2.1/docs/guide/api-reference.md +366 -0
- malwar-0.2.1/docs/guide/cli-reference.md +378 -0
- malwar-0.2.1/docs/guide/detection-rules.md +297 -0
- malwar-0.2.1/docs/guide/threat-campaigns.md +189 -0
- malwar-0.2.1/docs/images/campaign-detail.png +0 -0
- malwar-0.2.1/docs/images/campaigns.png +0 -0
- malwar-0.2.1/docs/images/dashboard.png +0 -0
- malwar-0.2.1/docs/images/scan-detail.png +0 -0
- malwar-0.2.1/docs/images/scan.png +0 -0
- malwar-0.2.1/docs/images/scans.png +0 -0
- malwar-0.2.1/docs/images/signatures.png +0 -0
- malwar-0.2.1/docs/index.md +123 -0
- malwar-0.2.1/docs/integrations/campaign-ingestion.md +99 -0
- malwar-0.2.1/docs/integrations/github-action.md +162 -0
- malwar-0.2.1/docs/integrations/langchain.md +208 -0
- malwar-0.2.1/docs/integrations/sdk.md +157 -0
- malwar-0.2.1/docs/integrations/stix-taxii.md +107 -0
- malwar-0.2.1/docs/kubernetes.md +327 -0
- malwar-0.2.1/docs/langchain.md +333 -0
- malwar-0.2.1/docs/product-brief.md +92 -0
- malwar-0.2.1/docs/sdk.md +169 -0
- malwar-0.2.1/docs/stix-taxii.md +213 -0
- malwar-0.2.1/docs/threat-campaigns.md +526 -0
- malwar-0.2.1/mkdocs.yml +76 -0
- malwar-0.2.1/pyproject.toml +126 -0
- malwar-0.2.1/src/malwar/__init__.py +25 -0
- malwar-0.2.1/src/malwar/__main__.py +6 -0
- malwar-0.2.1/src/malwar/api/__init__.py +1 -0
- malwar-0.2.1/src/malwar/api/app.py +90 -0
- malwar-0.2.1/src/malwar/api/auth.py +35 -0
- malwar-0.2.1/src/malwar/api/middleware.py +181 -0
- malwar-0.2.1/src/malwar/api/routes/__init__.py +1 -0
- malwar-0.2.1/src/malwar/api/routes/analytics.py +166 -0
- malwar-0.2.1/src/malwar/api/routes/campaigns.py +115 -0
- malwar-0.2.1/src/malwar/api/routes/export.py +136 -0
- malwar-0.2.1/src/malwar/api/routes/feed.py +229 -0
- malwar-0.2.1/src/malwar/api/routes/health.py +38 -0
- malwar-0.2.1/src/malwar/api/routes/ingest.py +76 -0
- malwar-0.2.1/src/malwar/api/routes/reports.py +205 -0
- malwar-0.2.1/src/malwar/api/routes/scan.py +341 -0
- malwar-0.2.1/src/malwar/api/routes/signatures.py +192 -0
- malwar-0.2.1/src/malwar/cli/__init__.py +1 -0
- malwar-0.2.1/src/malwar/cli/app.py +439 -0
- malwar-0.2.1/src/malwar/cli/commands/__init__.py +1 -0
- malwar-0.2.1/src/malwar/cli/commands/db.py +103 -0
- malwar-0.2.1/src/malwar/cli/commands/export.py +99 -0
- malwar-0.2.1/src/malwar/cli/commands/ingest.py +152 -0
- malwar-0.2.1/src/malwar/cli/formatters/__init__.py +1 -0
- malwar-0.2.1/src/malwar/cli/formatters/console.py +102 -0
- malwar-0.2.1/src/malwar/cli/formatters/json_fmt.py +30 -0
- malwar-0.2.1/src/malwar/cli/formatters/sarif.py +88 -0
- malwar-0.2.1/src/malwar/core/__init__.py +1 -0
- malwar-0.2.1/src/malwar/core/config.py +88 -0
- malwar-0.2.1/src/malwar/core/constants.py +54 -0
- malwar-0.2.1/src/malwar/core/exceptions.py +38 -0
- malwar-0.2.1/src/malwar/core/logging.py +56 -0
- malwar-0.2.1/src/malwar/detectors/__init__.py +1 -0
- malwar-0.2.1/src/malwar/detectors/llm_analyzer/__init__.py +1 -0
- malwar-0.2.1/src/malwar/detectors/llm_analyzer/detector.py +137 -0
- malwar-0.2.1/src/malwar/detectors/llm_analyzer/parser.py +189 -0
- malwar-0.2.1/src/malwar/detectors/llm_analyzer/prompts.py +95 -0
- malwar-0.2.1/src/malwar/detectors/rule_engine/__init__.py +1 -0
- malwar-0.2.1/src/malwar/detectors/rule_engine/base_rule.py +26 -0
- malwar-0.2.1/src/malwar/detectors/rule_engine/detector.py +63 -0
- malwar-0.2.1/src/malwar/detectors/rule_engine/registry.py +43 -0
- malwar-0.2.1/src/malwar/detectors/rule_engine/rules/__init__.py +1 -0
- malwar-0.2.1/src/malwar/detectors/rule_engine/rules/agent_hijacking.py +83 -0
- malwar-0.2.1/src/malwar/detectors/rule_engine/rules/credential_exposure.py +102 -0
- malwar-0.2.1/src/malwar/detectors/rule_engine/rules/env_harvesting.py +93 -0
- malwar-0.2.1/src/malwar/detectors/rule_engine/rules/exfiltration.py +150 -0
- malwar-0.2.1/src/malwar/detectors/rule_engine/rules/known_malware.py +117 -0
- malwar-0.2.1/src/malwar/detectors/rule_engine/rules/multi_step.py +93 -0
- malwar-0.2.1/src/malwar/detectors/rule_engine/rules/obfuscation.py +138 -0
- malwar-0.2.1/src/malwar/detectors/rule_engine/rules/persistence.py +163 -0
- malwar-0.2.1/src/malwar/detectors/rule_engine/rules/prompt_injection.py +136 -0
- malwar-0.2.1/src/malwar/detectors/rule_engine/rules/social_engineering.py +193 -0
- malwar-0.2.1/src/malwar/detectors/rule_engine/rules/steganography.py +169 -0
- malwar-0.2.1/src/malwar/detectors/rule_engine/rules/supply_chain.py +168 -0
- malwar-0.2.1/src/malwar/detectors/rule_engine/rules/suspicious_commands.py +180 -0
- malwar-0.2.1/src/malwar/detectors/threat_intel/__init__.py +1 -0
- malwar-0.2.1/src/malwar/detectors/threat_intel/detector.py +55 -0
- malwar-0.2.1/src/malwar/detectors/threat_intel/matcher.py +299 -0
- malwar-0.2.1/src/malwar/detectors/url_crawler/__init__.py +6 -0
- malwar-0.2.1/src/malwar/detectors/url_crawler/analyzer.py +247 -0
- malwar-0.2.1/src/malwar/detectors/url_crawler/detector.py +144 -0
- malwar-0.2.1/src/malwar/detectors/url_crawler/extractor.py +113 -0
- malwar-0.2.1/src/malwar/detectors/url_crawler/fetcher.py +222 -0
- malwar-0.2.1/src/malwar/detectors/url_crawler/reputation.py +138 -0
- malwar-0.2.1/src/malwar/export/__init__.py +24 -0
- malwar-0.2.1/src/malwar/export/stix.py +330 -0
- malwar-0.2.1/src/malwar/export/taxii.py +139 -0
- malwar-0.2.1/src/malwar/ingestion/__init__.py +27 -0
- malwar-0.2.1/src/malwar/ingestion/importer.py +238 -0
- malwar-0.2.1/src/malwar/ingestion/schema.py +63 -0
- malwar-0.2.1/src/malwar/ingestion/sources.py +479 -0
- malwar-0.2.1/src/malwar/integrations/__init__.py +16 -0
- malwar-0.2.1/src/malwar/integrations/exceptions.py +40 -0
- malwar-0.2.1/src/malwar/integrations/langchain.py +419 -0
- malwar-0.2.1/src/malwar/models/__init__.py +17 -0
- malwar-0.2.1/src/malwar/models/finding.py +39 -0
- malwar-0.2.1/src/malwar/models/report.py +27 -0
- malwar-0.2.1/src/malwar/models/sarif.py +70 -0
- malwar-0.2.1/src/malwar/models/scan.py +78 -0
- malwar-0.2.1/src/malwar/models/signature.py +43 -0
- malwar-0.2.1/src/malwar/models/skill.py +57 -0
- malwar-0.2.1/src/malwar/notifications/__init__.py +6 -0
- malwar-0.2.1/src/malwar/notifications/webhook.py +140 -0
- malwar-0.2.1/src/malwar/parsers/__init__.py +13 -0
- malwar-0.2.1/src/malwar/parsers/markdown_parser.py +125 -0
- malwar-0.2.1/src/malwar/parsers/skill_parser.py +121 -0
- malwar-0.2.1/src/malwar/py.typed +0 -0
- malwar-0.2.1/src/malwar/scanner/__init__.py +1 -0
- malwar-0.2.1/src/malwar/scanner/base.py +36 -0
- malwar-0.2.1/src/malwar/scanner/context.py +48 -0
- malwar-0.2.1/src/malwar/scanner/pipeline.py +144 -0
- malwar-0.2.1/src/malwar/scanner/severity.py +35 -0
- malwar-0.2.1/src/malwar/sdk.py +269 -0
- malwar-0.2.1/src/malwar/storage/__init__.py +22 -0
- malwar-0.2.1/src/malwar/storage/database.py +67 -0
- malwar-0.2.1/src/malwar/storage/migrations.py +532 -0
- malwar-0.2.1/src/malwar/storage/repositories/__init__.py +16 -0
- malwar-0.2.1/src/malwar/storage/repositories/campaigns.py +33 -0
- malwar-0.2.1/src/malwar/storage/repositories/findings.py +74 -0
- malwar-0.2.1/src/malwar/storage/repositories/publishers.py +44 -0
- malwar-0.2.1/src/malwar/storage/repositories/scans.py +132 -0
- malwar-0.2.1/src/malwar/storage/repositories/signatures.py +133 -0
- malwar-0.2.1/tests/__init__.py +0 -0
- malwar-0.2.1/tests/benchmark/__init__.py +1 -0
- malwar-0.2.1/tests/benchmark/test_accuracy.py +233 -0
- malwar-0.2.1/tests/benchmark/test_benchmarks.py +221 -0
- malwar-0.2.1/tests/conftest.py +30 -0
- malwar-0.2.1/tests/e2e/__init__.py +0 -0
- malwar-0.2.1/tests/e2e/test_full_scan.py +343 -0
- malwar-0.2.1/tests/fixtures/skills/benign/code_formatter.md +58 -0
- malwar-0.2.1/tests/fixtures/skills/benign/git_helper.md +65 -0
- malwar-0.2.1/tests/fixtures/skills/benign/hello_world.md +26 -0
- malwar-0.2.1/tests/fixtures/skills/benign/legitimate_with_urls.md +45 -0
- malwar-0.2.1/tests/fixtures/skills/benign/web_search.md +54 -0
- malwar-0.2.1/tests/fixtures/skills/malicious/agent_hijacking.md +33 -0
- malwar-0.2.1/tests/fixtures/skills/malicious/base64_reverse_shell.md +39 -0
- malwar-0.2.1/tests/fixtures/skills/malicious/clawhavoc_amos.md +60 -0
- malwar-0.2.1/tests/fixtures/skills/malicious/clickfix_fake_prereq.md +51 -0
- malwar-0.2.1/tests/fixtures/skills/malicious/credential_harvester.md +67 -0
- malwar-0.2.1/tests/fixtures/skills/malicious/env_harvesting.md +46 -0
- malwar-0.2.1/tests/fixtures/skills/malicious/exfil_soul_md.md +50 -0
- malwar-0.2.1/tests/fixtures/skills/malicious/filesystem_modification.md +44 -0
- malwar-0.2.1/tests/fixtures/skills/malicious/multi_stage_dropper.md +66 -0
- malwar-0.2.1/tests/fixtures/skills/malicious/multi_step_manipulation.md +39 -0
- malwar-0.2.1/tests/fixtures/skills/malicious/obfuscated_curl.md +57 -0
- malwar-0.2.1/tests/fixtures/skills/malicious/persistence_mechanism.md +53 -0
- malwar-0.2.1/tests/fixtures/skills/malicious/prompt_injection_basic.md +53 -0
- malwar-0.2.1/tests/fixtures/skills/malicious/prompt_injection_unicode.md +49 -0
- malwar-0.2.1/tests/fixtures/skills/malicious/shadowpkg_typosquat.md +64 -0
- malwar-0.2.1/tests/fixtures/skills/malicious/snyktoxic_vercel_exfil.md +58 -0
- malwar-0.2.1/tests/fixtures/skills/malicious/steganographic.md +27 -0
- malwar-0.2.1/tests/fixtures/skills/malicious/supply_chain.md +41 -0
- malwar-0.2.1/tests/fixtures/skills/malicious/typosquatted_package.md +57 -0
- malwar-0.2.1/tests/fixtures/skills/real/benign/anthropic_frontend_design.md +42 -0
- malwar-0.2.1/tests/fixtures/skills/real/benign/anthropic_pdf.md +314 -0
- malwar-0.2.1/tests/fixtures/skills/real/benign/anthropic_skill_creator.md +357 -0
- malwar-0.2.1/tests/fixtures/skills/real/clawhub/bankrbot_bankr.md +821 -0
- malwar-0.2.1/tests/fixtures/skills/real/clawhub/bankrbot_clanker.md +463 -0
- malwar-0.2.1/tests/fixtures/skills/real/clawhub/bankrbot_onchainkit.md +364 -0
- malwar-0.2.1/tests/fixtures/skills/real/clawhub/bankrbot_veil.md +111 -0
- malwar-0.2.1/tests/fixtures/skills/real/clawhub/bankrbot_yoink.md +76 -0
- malwar-0.2.1/tests/fixtures/skills/real/clawhub/bankrbot_zapper.md +4 -0
- malwar-0.2.1/tests/fixtures/skills/real/clawhub/clawpod_massive.md +188 -0
- malwar-0.2.1/tests/fixtures/skills/real/clawhub/metamask_gator.md +235 -0
- malwar-0.2.1/tests/fixtures/skills/real/clawhub/metamask_smart_accounts.md +754 -0
- malwar-0.2.1/tests/fixtures/skills/real/clawhub/model_hierarchy.md +280 -0
- malwar-0.2.1/tests/fixtures/skills/real/clawhub/search_layer.md +238 -0
- malwar-0.2.1/tests/fixtures/skills/real/clawhub/x_bookmarks.md +218 -0
- malwar-0.2.1/tests/fixtures/skills/real/clawhub/x_tweet_fetcher.md +113 -0
- malwar-0.2.1/tests/fixtures/skills/real/malicious/snyk_ascii_smuggling.md +16 -0
- malwar-0.2.1/tests/fixtures/skills/real/malicious/snyk_clawhub.md +482 -0
- malwar-0.2.1/tests/fixtures/skills/real/malicious/snyk_clawhub_agent.md +482 -0
- malwar-0.2.1/tests/fixtures/skills/real/malicious/snyk_gemini_vercel.md +38 -0
- malwar-0.2.1/tests/fixtures/skills/real/malicious/snyk_google_malware.md +334 -0
- malwar-0.2.1/tests/fixtures/skills/real/malicious/snyk_vercel_exfil.md +38 -0
- malwar-0.2.1/tests/integration/__init__.py +0 -0
- malwar-0.2.1/tests/integration/test_analytics.py +150 -0
- malwar-0.2.1/tests/integration/test_api.py +459 -0
- malwar-0.2.1/tests/integration/test_batch_scan.py +164 -0
- malwar-0.2.1/tests/integration/test_campaigns_api.py +92 -0
- malwar-0.2.1/tests/integration/test_export_api.py +191 -0
- malwar-0.2.1/tests/integration/test_feed.py +302 -0
- malwar-0.2.1/tests/integration/test_ingest_api.py +249 -0
- malwar-0.2.1/tests/integration/test_reports_api.py +231 -0
- malwar-0.2.1/tests/integration/test_scan_pipeline.py +161 -0
- malwar-0.2.1/tests/integration/test_signatures_api.py +229 -0
- malwar-0.2.1/tests/integration/test_webhooks.py +333 -0
- malwar-0.2.1/tests/live/__init__.py +1 -0
- malwar-0.2.1/tests/live/test_llm_live.py +247 -0
- malwar-0.2.1/tests/unit/__init__.py +0 -0
- malwar-0.2.1/tests/unit/detectors/__init__.py +0 -0
- malwar-0.2.1/tests/unit/detectors/test_llm_analyzer.py +655 -0
- malwar-0.2.1/tests/unit/detectors/test_rule_engine.py +763 -0
- malwar-0.2.1/tests/unit/detectors/test_threat_intel.py +391 -0
- malwar-0.2.1/tests/unit/detectors/test_url_crawler.py +584 -0
- malwar-0.2.1/tests/unit/test_core_models.py +811 -0
- malwar-0.2.1/tests/unit/test_github_action.py +435 -0
- malwar-0.2.1/tests/unit/test_helm_chart.py +257 -0
- malwar-0.2.1/tests/unit/test_ingestion.py +683 -0
- malwar-0.2.1/tests/unit/test_langchain_integration.py +444 -0
- malwar-0.2.1/tests/unit/test_markdown_parser.py +287 -0
- malwar-0.2.1/tests/unit/test_migrations.py +278 -0
- malwar-0.2.1/tests/unit/test_rate_limit.py +191 -0
- malwar-0.2.1/tests/unit/test_sdk.py +266 -0
- malwar-0.2.1/tests/unit/test_skill_parser.py +276 -0
- malwar-0.2.1/tests/unit/test_stix_export.py +539 -0
- malwar-0.2.1/tests/unit/test_storage.py +485 -0
- malwar-0.2.1/tests/unit/test_webhook.py +297 -0
- malwar-0.2.1/web/.gitignore +24 -0
- malwar-0.2.1/web/README.md +73 -0
- malwar-0.2.1/web/eslint.config.js +23 -0
- malwar-0.2.1/web/index.html +13 -0
- malwar-0.2.1/web/package-lock.json +4412 -0
- malwar-0.2.1/web/package.json +38 -0
- malwar-0.2.1/web/public/vite.svg +1 -0
- malwar-0.2.1/web/src/App.tsx +29 -0
- malwar-0.2.1/web/src/components/Badge.tsx +32 -0
- malwar-0.2.1/web/src/components/Card.tsx +35 -0
- malwar-0.2.1/web/src/components/Layout.tsx +134 -0
- malwar-0.2.1/web/src/components/LoadingSpinner.tsx +42 -0
- malwar-0.2.1/web/src/index.css +47 -0
- malwar-0.2.1/web/src/lib/api.ts +150 -0
- malwar-0.2.1/web/src/lib/utils.ts +60 -0
- malwar-0.2.1/web/src/main.tsx +10 -0
- malwar-0.2.1/web/src/pages/CampaignsPage.tsx +408 -0
- malwar-0.2.1/web/src/pages/Dashboard.tsx +431 -0
- malwar-0.2.1/web/src/pages/ScanDetail.tsx +477 -0
- malwar-0.2.1/web/src/pages/ScanPage.tsx +491 -0
- malwar-0.2.1/web/src/pages/ScansHistory.tsx +501 -0
- malwar-0.2.1/web/src/pages/SignaturesPage.tsx +482 -0
- malwar-0.2.1/web/tsconfig.app.json +28 -0
- malwar-0.2.1/web/tsconfig.json +7 -0
- malwar-0.2.1/web/tsconfig.node.json +26 -0
- malwar-0.2.1/web/vite.config.ts +27 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Copyright (c) 2026 Veritas Aequitas Holdings LLC. All rights reserved.
|
|
2
|
+
.git
|
|
3
|
+
__pycache__
|
|
4
|
+
.venv
|
|
5
|
+
node_modules
|
|
6
|
+
web/
|
|
7
|
+
tests/
|
|
8
|
+
docs/
|
|
9
|
+
.github/
|
|
10
|
+
*.pyc
|
|
11
|
+
.env
|
|
12
|
+
.env.*
|
|
13
|
+
.mypy_cache
|
|
14
|
+
.ruff_cache
|
|
15
|
+
.pytest_cache
|
|
16
|
+
htmlcov/
|
|
17
|
+
*.db
|
|
18
|
+
deploy/
|
|
19
|
+
.claude/
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# malwar configuration
|
|
2
|
+
# Copy this file to .env and fill in values
|
|
3
|
+
|
|
4
|
+
# Anthropic API key for LLM semantic analysis (Layer 3)
|
|
5
|
+
MALWAR_ANTHROPIC_API_KEY=
|
|
6
|
+
|
|
7
|
+
# API authentication keys (comma-separated)
|
|
8
|
+
MALWAR_API_KEYS=
|
|
9
|
+
|
|
10
|
+
# Database path (default: malwar.db in current directory)
|
|
11
|
+
MALWAR_DB_PATH=malwar.db
|
|
12
|
+
|
|
13
|
+
# API server settings
|
|
14
|
+
MALWAR_API_HOST=127.0.0.1
|
|
15
|
+
MALWAR_API_PORT=8000
|
|
16
|
+
|
|
17
|
+
# LLM settings
|
|
18
|
+
MALWAR_LLM_MODEL=claude-sonnet-4-20250514
|
|
19
|
+
MALWAR_LLM_SKIP_BELOW_RISK=15
|
|
20
|
+
|
|
21
|
+
# URL crawler settings
|
|
22
|
+
MALWAR_CRAWLER_MAX_URLS=10
|
|
23
|
+
MALWAR_CRAWLER_TIMEOUT=5.0
|
|
24
|
+
|
|
25
|
+
# Logging
|
|
26
|
+
MALWAR_LOG_LEVEL=INFO
|
|
27
|
+
MALWAR_LOG_FORMAT=json
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
github: [Ap6pack]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: Bug Report
|
|
2
|
+
description: Report a bug in Malwar
|
|
3
|
+
labels: ["bug"]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: description
|
|
7
|
+
attributes:
|
|
8
|
+
label: Description
|
|
9
|
+
description: What happened?
|
|
10
|
+
placeholder: A clear description of the bug
|
|
11
|
+
validations:
|
|
12
|
+
required: true
|
|
13
|
+
- type: textarea
|
|
14
|
+
id: steps
|
|
15
|
+
attributes:
|
|
16
|
+
label: Steps to Reproduce
|
|
17
|
+
description: How can we reproduce this?
|
|
18
|
+
placeholder: |
|
|
19
|
+
1. Run `malwar scan ...`
|
|
20
|
+
2. ...
|
|
21
|
+
validations:
|
|
22
|
+
required: true
|
|
23
|
+
- type: textarea
|
|
24
|
+
id: expected
|
|
25
|
+
attributes:
|
|
26
|
+
label: Expected Behavior
|
|
27
|
+
placeholder: What should have happened?
|
|
28
|
+
validations:
|
|
29
|
+
required: true
|
|
30
|
+
- type: textarea
|
|
31
|
+
id: actual
|
|
32
|
+
attributes:
|
|
33
|
+
label: Actual Behavior
|
|
34
|
+
placeholder: What actually happened?
|
|
35
|
+
validations:
|
|
36
|
+
required: true
|
|
37
|
+
- type: input
|
|
38
|
+
id: version
|
|
39
|
+
attributes:
|
|
40
|
+
label: Malwar Version
|
|
41
|
+
placeholder: "0.2.0"
|
|
42
|
+
validations:
|
|
43
|
+
required: true
|
|
44
|
+
- type: input
|
|
45
|
+
id: python
|
|
46
|
+
attributes:
|
|
47
|
+
label: Python Version
|
|
48
|
+
placeholder: "3.13"
|
|
49
|
+
- type: input
|
|
50
|
+
id: os
|
|
51
|
+
attributes:
|
|
52
|
+
label: Operating System
|
|
53
|
+
placeholder: "Ubuntu 24.04"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: Detection Rule Request
|
|
2
|
+
description: Suggest a new detection rule
|
|
3
|
+
labels: ["detection-rule", "enhancement"]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: description
|
|
7
|
+
attributes:
|
|
8
|
+
label: Description
|
|
9
|
+
description: What attack pattern should be detected?
|
|
10
|
+
placeholder: Describe the threat this rule would catch
|
|
11
|
+
validations:
|
|
12
|
+
required: true
|
|
13
|
+
- type: dropdown
|
|
14
|
+
id: attack_type
|
|
15
|
+
attributes:
|
|
16
|
+
label: Attack Type
|
|
17
|
+
options:
|
|
18
|
+
- Obfuscation
|
|
19
|
+
- Prompt Injection
|
|
20
|
+
- Social Engineering
|
|
21
|
+
- Data Exfiltration
|
|
22
|
+
- Credential Theft
|
|
23
|
+
- Malware Delivery
|
|
24
|
+
- Supply Chain
|
|
25
|
+
- Persistence
|
|
26
|
+
- Other
|
|
27
|
+
validations:
|
|
28
|
+
required: true
|
|
29
|
+
- type: textarea
|
|
30
|
+
id: example
|
|
31
|
+
attributes:
|
|
32
|
+
label: Example SKILL.md Content
|
|
33
|
+
description: Provide a sample that demonstrates the attack pattern
|
|
34
|
+
render: markdown
|
|
35
|
+
validations:
|
|
36
|
+
required: true
|
|
37
|
+
- type: dropdown
|
|
38
|
+
id: expected_verdict
|
|
39
|
+
attributes:
|
|
40
|
+
label: Expected Verdict
|
|
41
|
+
options:
|
|
42
|
+
- MALICIOUS
|
|
43
|
+
- SUSPICIOUS
|
|
44
|
+
- CAUTION
|
|
45
|
+
validations:
|
|
46
|
+
required: true
|
|
47
|
+
- type: dropdown
|
|
48
|
+
id: severity
|
|
49
|
+
attributes:
|
|
50
|
+
label: Suggested Severity
|
|
51
|
+
options:
|
|
52
|
+
- Critical
|
|
53
|
+
- High
|
|
54
|
+
- Medium
|
|
55
|
+
- Low
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Feature Request
|
|
2
|
+
description: Suggest a new feature for Malwar
|
|
3
|
+
labels: ["enhancement"]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: description
|
|
7
|
+
attributes:
|
|
8
|
+
label: Description
|
|
9
|
+
description: What feature would you like?
|
|
10
|
+
placeholder: A clear description of the feature
|
|
11
|
+
validations:
|
|
12
|
+
required: true
|
|
13
|
+
- type: textarea
|
|
14
|
+
id: use_case
|
|
15
|
+
attributes:
|
|
16
|
+
label: Use Case
|
|
17
|
+
description: Why do you need this?
|
|
18
|
+
placeholder: Describe the problem this would solve
|
|
19
|
+
validations:
|
|
20
|
+
required: true
|
|
21
|
+
- type: textarea
|
|
22
|
+
id: solution
|
|
23
|
+
attributes:
|
|
24
|
+
label: Proposed Solution
|
|
25
|
+
description: How would you implement this?
|
|
26
|
+
placeholder: Optional — your ideas on how to solve it
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
## Description
|
|
2
|
+
|
|
3
|
+
<!-- What does this PR do? -->
|
|
4
|
+
|
|
5
|
+
## Type of Change
|
|
6
|
+
|
|
7
|
+
- [ ] Bug fix
|
|
8
|
+
- [ ] New feature
|
|
9
|
+
- [ ] New detection rule
|
|
10
|
+
- [ ] Documentation
|
|
11
|
+
- [ ] Infrastructure / CI
|
|
12
|
+
|
|
13
|
+
## Checklist
|
|
14
|
+
|
|
15
|
+
- [ ] Tests added or updated
|
|
16
|
+
- [ ] `ruff check src/ tests/` passes
|
|
17
|
+
- [ ] `pytest tests/` passes
|
|
18
|
+
- [ ] Copyright headers on new files
|
|
19
|
+
- [ ] Documentation updated (if applicable)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Copyright (c) 2026 Veritas Aequitas Holdings LLC. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Composite GitHub Action that scans SKILL.md files for malware using Malwar.
|
|
4
|
+
|
|
5
|
+
name: "Malwar Skill Scanner"
|
|
6
|
+
description: "Scan SKILL.md files in pull requests for malware, prompt injection, and other threats."
|
|
7
|
+
branding:
|
|
8
|
+
icon: "shield"
|
|
9
|
+
color: "red"
|
|
10
|
+
|
|
11
|
+
inputs:
|
|
12
|
+
path:
|
|
13
|
+
description: "Glob pattern for SKILL.md files to scan"
|
|
14
|
+
required: false
|
|
15
|
+
default: "**/SKILL.md"
|
|
16
|
+
fail-on:
|
|
17
|
+
description: "Verdict threshold that causes the action to fail (MALICIOUS, SUSPICIOUS, CAUTION)"
|
|
18
|
+
required: false
|
|
19
|
+
default: "SUSPICIOUS"
|
|
20
|
+
format:
|
|
21
|
+
description: "Output format: text, json, or sarif"
|
|
22
|
+
required: false
|
|
23
|
+
default: "text"
|
|
24
|
+
|
|
25
|
+
outputs:
|
|
26
|
+
verdict:
|
|
27
|
+
description: "Worst verdict across all scanned files"
|
|
28
|
+
value: ${{ steps.scan.outputs.verdict }}
|
|
29
|
+
risk_score:
|
|
30
|
+
description: "Highest risk score across all scanned files"
|
|
31
|
+
value: ${{ steps.scan.outputs.risk_score }}
|
|
32
|
+
finding_count:
|
|
33
|
+
description: "Total number of findings across all scanned files"
|
|
34
|
+
value: ${{ steps.scan.outputs.finding_count }}
|
|
35
|
+
sarif_path:
|
|
36
|
+
description: "Path to SARIF output file (when format is sarif)"
|
|
37
|
+
value: ${{ steps.scan.outputs.sarif_path }}
|
|
38
|
+
|
|
39
|
+
runs:
|
|
40
|
+
using: "composite"
|
|
41
|
+
steps:
|
|
42
|
+
- name: Set up Python 3.13
|
|
43
|
+
uses: actions/setup-python@v5
|
|
44
|
+
with:
|
|
45
|
+
python-version: "3.13"
|
|
46
|
+
|
|
47
|
+
- name: Install Malwar
|
|
48
|
+
shell: bash
|
|
49
|
+
run: pip install malwar
|
|
50
|
+
|
|
51
|
+
- name: Run Malwar scan
|
|
52
|
+
id: scan
|
|
53
|
+
shell: bash
|
|
54
|
+
run: |
|
|
55
|
+
python "${{ github.action_path }}/scan.py" \
|
|
56
|
+
--path "${{ inputs.path }}" \
|
|
57
|
+
--fail-on "${{ inputs.fail-on }}" \
|
|
58
|
+
--format "${{ inputs.format }}"
|
|
59
|
+
|
|
60
|
+
- name: Post PR comment
|
|
61
|
+
if: github.event_name == 'pull_request' && always()
|
|
62
|
+
uses: actions/github-script@v7
|
|
63
|
+
with:
|
|
64
|
+
script: |
|
|
65
|
+
const verdict = '${{ steps.scan.outputs.verdict }}';
|
|
66
|
+
const riskScore = '${{ steps.scan.outputs.risk_score }}';
|
|
67
|
+
const findingCount = '${{ steps.scan.outputs.finding_count }}';
|
|
68
|
+
|
|
69
|
+
if (!verdict) return;
|
|
70
|
+
|
|
71
|
+
const icons = {
|
|
72
|
+
'CLEAN': ':white_check_mark:',
|
|
73
|
+
'CAUTION': ':warning:',
|
|
74
|
+
'SUSPICIOUS': ':orange_circle:',
|
|
75
|
+
'MALICIOUS': ':red_circle:'
|
|
76
|
+
};
|
|
77
|
+
const icon = icons[verdict] || ':question:';
|
|
78
|
+
|
|
79
|
+
const body = [
|
|
80
|
+
`## ${icon} Malwar Scan Results`,
|
|
81
|
+
'',
|
|
82
|
+
`| Metric | Value |`,
|
|
83
|
+
`|--------|-------|`,
|
|
84
|
+
`| **Verdict** | ${verdict} |`,
|
|
85
|
+
`| **Risk Score** | ${riskScore}/100 |`,
|
|
86
|
+
`| **Findings** | ${findingCount} |`,
|
|
87
|
+
'',
|
|
88
|
+
'*Scanned by [Malwar](https://github.com/Ap6pack/malwar) — malware detection for agentic AI skills.*'
|
|
89
|
+
].join('\n');
|
|
90
|
+
|
|
91
|
+
const { data: comments } = await github.rest.issues.listComments({
|
|
92
|
+
owner: context.repo.owner,
|
|
93
|
+
repo: context.repo.repo,
|
|
94
|
+
issue_number: context.issue.number,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const existing = comments.find(c =>
|
|
98
|
+
c.user.type === 'Bot' && c.body.includes('Malwar Scan Results')
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (existing) {
|
|
102
|
+
await github.rest.issues.updateComment({
|
|
103
|
+
owner: context.repo.owner,
|
|
104
|
+
repo: context.repo.repo,
|
|
105
|
+
comment_id: existing.id,
|
|
106
|
+
body,
|
|
107
|
+
});
|
|
108
|
+
} else {
|
|
109
|
+
await github.rest.issues.createComment({
|
|
110
|
+
owner: context.repo.owner,
|
|
111
|
+
repo: context.repo.repo,
|
|
112
|
+
issue_number: context.issue.number,
|
|
113
|
+
body,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# Copyright (c) 2026 Veritas Aequitas Holdings LLC. All rights reserved.
|
|
2
|
+
"""Malwar SKILL.md scanner for GitHub Actions.
|
|
3
|
+
|
|
4
|
+
Finds files matching a glob pattern, scans each with the malwar SDK,
|
|
5
|
+
and reports results in the requested format. Sets GitHub Action outputs
|
|
6
|
+
and exits with code 1 if any file meets or exceeds the fail-on threshold.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
from glob import glob
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from malwar import __version__, scan
|
|
22
|
+
from malwar.cli.formatters.sarif import scan_result_to_sarif
|
|
23
|
+
from malwar.models.scan import ScanResult
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger("malwar.action")
|
|
26
|
+
|
|
27
|
+
# Verdict severity ordering (lowest to highest)
|
|
28
|
+
VERDICT_ORDER: dict[str, int] = {
|
|
29
|
+
"CLEAN": 0,
|
|
30
|
+
"CAUTION": 1,
|
|
31
|
+
"SUSPICIOUS": 2,
|
|
32
|
+
"MALICIOUS": 3,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
SARIF_OUTPUT_FILE = "malwar-results.sarif"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
39
|
+
"""Parse command-line arguments."""
|
|
40
|
+
parser = argparse.ArgumentParser(
|
|
41
|
+
description="Scan SKILL.md files for threats using Malwar.",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--path",
|
|
45
|
+
default="**/SKILL.md",
|
|
46
|
+
help="Glob pattern for files to scan (default: **/SKILL.md)",
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--fail-on",
|
|
50
|
+
default="SUSPICIOUS",
|
|
51
|
+
choices=["CAUTION", "SUSPICIOUS", "MALICIOUS"],
|
|
52
|
+
help="Verdict threshold that triggers failure (default: SUSPICIOUS)",
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--format",
|
|
56
|
+
default="text",
|
|
57
|
+
choices=["text", "json", "sarif"],
|
|
58
|
+
dest="output_format",
|
|
59
|
+
help="Output format (default: text)",
|
|
60
|
+
)
|
|
61
|
+
return parser.parse_args(argv)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def find_files(pattern: str) -> list[Path]:
|
|
65
|
+
"""Find all files matching the glob pattern."""
|
|
66
|
+
matches = sorted(glob(pattern, recursive=True))
|
|
67
|
+
return [Path(m) for m in matches if Path(m).is_file()]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def verdict_meets_threshold(verdict: str, threshold: str) -> bool:
|
|
71
|
+
"""Return True if the verdict meets or exceeds the threshold."""
|
|
72
|
+
return VERDICT_ORDER.get(verdict, 0) >= VERDICT_ORDER.get(threshold, 0)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def worst_verdict(verdicts: list[str]) -> str:
|
|
76
|
+
"""Return the most severe verdict from a list."""
|
|
77
|
+
if not verdicts:
|
|
78
|
+
return "CLEAN"
|
|
79
|
+
return max(verdicts, key=lambda v: VERDICT_ORDER.get(v, 0))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def scan_files(files: list[Path]) -> list[tuple[Path, ScanResult]]:
|
|
83
|
+
"""Scan all files and return (path, result) pairs."""
|
|
84
|
+
results: list[tuple[Path, ScanResult]] = []
|
|
85
|
+
for file_path in files:
|
|
86
|
+
content = file_path.read_text(encoding="utf-8")
|
|
87
|
+
result = await scan(
|
|
88
|
+
content,
|
|
89
|
+
file_name=str(file_path),
|
|
90
|
+
use_llm=False,
|
|
91
|
+
use_urls=False,
|
|
92
|
+
)
|
|
93
|
+
results.append((file_path, result))
|
|
94
|
+
return results
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def format_text(results: list[tuple[Path, ScanResult]]) -> str:
|
|
98
|
+
"""Format scan results as human-readable text."""
|
|
99
|
+
if not results:
|
|
100
|
+
return "No SKILL.md files found to scan."
|
|
101
|
+
|
|
102
|
+
lines: list[str] = []
|
|
103
|
+
lines.append(f"Malwar Scan Results (v{__version__})")
|
|
104
|
+
lines.append("=" * 50)
|
|
105
|
+
|
|
106
|
+
for file_path, result in results:
|
|
107
|
+
lines.append("")
|
|
108
|
+
lines.append(f"File: {file_path}")
|
|
109
|
+
lines.append(f" Verdict: {result.verdict}")
|
|
110
|
+
lines.append(f" Risk Score: {result.risk_score}/100")
|
|
111
|
+
lines.append(f" Findings: {len(result.findings)}")
|
|
112
|
+
|
|
113
|
+
for finding in result.findings:
|
|
114
|
+
lines.append(f" - [{finding.severity.upper()}] {finding.title}")
|
|
115
|
+
if finding.location:
|
|
116
|
+
lines.append(f" Line {finding.location.line_start}: {finding.description}")
|
|
117
|
+
else:
|
|
118
|
+
lines.append(f" {finding.description}")
|
|
119
|
+
|
|
120
|
+
lines.append("")
|
|
121
|
+
lines.append("-" * 50)
|
|
122
|
+
all_verdicts = [r.verdict for _, r in results]
|
|
123
|
+
lines.append(f"Overall: {worst_verdict(all_verdicts)}")
|
|
124
|
+
|
|
125
|
+
return "\n".join(lines)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def format_json(results: list[tuple[Path, ScanResult]]) -> str:
|
|
129
|
+
"""Format scan results as JSON."""
|
|
130
|
+
output: list[dict[str, Any]] = []
|
|
131
|
+
for file_path, result in results:
|
|
132
|
+
output.append({
|
|
133
|
+
"file": str(file_path),
|
|
134
|
+
"verdict": result.verdict,
|
|
135
|
+
"risk_score": result.risk_score,
|
|
136
|
+
"finding_count": len(result.findings),
|
|
137
|
+
"findings": [
|
|
138
|
+
{
|
|
139
|
+
"id": f.id,
|
|
140
|
+
"rule_id": f.rule_id,
|
|
141
|
+
"title": f.title,
|
|
142
|
+
"severity": f.severity,
|
|
143
|
+
"confidence": f.confidence,
|
|
144
|
+
"description": f.description,
|
|
145
|
+
}
|
|
146
|
+
for f in result.findings
|
|
147
|
+
],
|
|
148
|
+
})
|
|
149
|
+
return json.dumps(output, indent=2)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def format_sarif(results: list[tuple[Path, ScanResult]]) -> str:
|
|
153
|
+
"""Format scan results as SARIF 2.1.0 and write to file."""
|
|
154
|
+
if not results:
|
|
155
|
+
sarif: dict[str, Any] = {
|
|
156
|
+
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
|
|
157
|
+
"version": "2.1.0",
|
|
158
|
+
"runs": [{
|
|
159
|
+
"tool": {
|
|
160
|
+
"driver": {
|
|
161
|
+
"name": "malwar",
|
|
162
|
+
"version": __version__,
|
|
163
|
+
"rules": [],
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
"results": [],
|
|
167
|
+
}],
|
|
168
|
+
}
|
|
169
|
+
elif len(results) == 1:
|
|
170
|
+
_, result = results[0]
|
|
171
|
+
sarif = scan_result_to_sarif(result)
|
|
172
|
+
else:
|
|
173
|
+
# Merge multiple results into a single SARIF run
|
|
174
|
+
all_rules: list[dict[str, Any]] = []
|
|
175
|
+
all_results: list[dict[str, Any]] = []
|
|
176
|
+
seen_rules: set[str] = set()
|
|
177
|
+
|
|
178
|
+
for _, result in results:
|
|
179
|
+
single = scan_result_to_sarif(result)
|
|
180
|
+
run = single["runs"][0]
|
|
181
|
+
for rule in run["tool"]["driver"]["rules"]:
|
|
182
|
+
if rule["id"] not in seen_rules:
|
|
183
|
+
seen_rules.add(rule["id"])
|
|
184
|
+
all_rules.append(rule)
|
|
185
|
+
all_results.extend(run["results"])
|
|
186
|
+
|
|
187
|
+
sarif = {
|
|
188
|
+
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
|
|
189
|
+
"version": "2.1.0",
|
|
190
|
+
"runs": [{
|
|
191
|
+
"tool": {
|
|
192
|
+
"driver": {
|
|
193
|
+
"name": "malwar",
|
|
194
|
+
"version": __version__,
|
|
195
|
+
"rules": all_rules,
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
"results": all_results,
|
|
199
|
+
}],
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
sarif_str = json.dumps(sarif, indent=2)
|
|
203
|
+
|
|
204
|
+
# Write SARIF file to disk
|
|
205
|
+
Path(SARIF_OUTPUT_FILE).write_text(sarif_str, encoding="utf-8")
|
|
206
|
+
|
|
207
|
+
return sarif_str
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def set_github_output(name: str, value: str) -> None:
|
|
211
|
+
"""Write a key=value pair to $GITHUB_OUTPUT."""
|
|
212
|
+
output_file = os.environ.get("GITHUB_OUTPUT")
|
|
213
|
+
if output_file:
|
|
214
|
+
with open(output_file, "a", encoding="utf-8") as f:
|
|
215
|
+
f.write(f"{name}={value}\n")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def main(argv: list[str] | None = None) -> int:
|
|
219
|
+
"""Entry point for the scan script."""
|
|
220
|
+
args = parse_args(argv)
|
|
221
|
+
|
|
222
|
+
# Discover files
|
|
223
|
+
files = find_files(args.path)
|
|
224
|
+
sys.stdout.write(f"Found {len(files)} file(s) matching '{args.path}'\n")
|
|
225
|
+
|
|
226
|
+
if not files:
|
|
227
|
+
set_github_output("verdict", "CLEAN")
|
|
228
|
+
set_github_output("risk_score", "0")
|
|
229
|
+
set_github_output("finding_count", "0")
|
|
230
|
+
sys.stdout.write("No files to scan. Exiting clean.\n")
|
|
231
|
+
return 0
|
|
232
|
+
|
|
233
|
+
# Run scans
|
|
234
|
+
results = asyncio.run(scan_files(files))
|
|
235
|
+
|
|
236
|
+
# Compute aggregates
|
|
237
|
+
verdicts = [r.verdict for _, r in results]
|
|
238
|
+
overall = worst_verdict(verdicts)
|
|
239
|
+
max_score = max((r.risk_score for _, r in results), default=0)
|
|
240
|
+
total_findings = sum(len(r.findings) for _, r in results)
|
|
241
|
+
|
|
242
|
+
# Format output
|
|
243
|
+
formatters = {
|
|
244
|
+
"text": format_text,
|
|
245
|
+
"json": format_json,
|
|
246
|
+
"sarif": format_sarif,
|
|
247
|
+
}
|
|
248
|
+
formatter = formatters[args.output_format]
|
|
249
|
+
output = formatter(results)
|
|
250
|
+
sys.stdout.write(output + "\n")
|
|
251
|
+
|
|
252
|
+
# Set GitHub Action outputs
|
|
253
|
+
set_github_output("verdict", overall)
|
|
254
|
+
set_github_output("risk_score", str(max_score))
|
|
255
|
+
set_github_output("finding_count", str(total_findings))
|
|
256
|
+
if args.output_format == "sarif":
|
|
257
|
+
set_github_output("sarif_path", SARIF_OUTPUT_FILE)
|
|
258
|
+
|
|
259
|
+
# Determine exit code
|
|
260
|
+
should_fail = verdict_meets_threshold(overall, args.fail_on)
|
|
261
|
+
if should_fail:
|
|
262
|
+
sys.stdout.write(f"\nFailed: verdict '{overall}' meets or exceeds threshold '{args.fail_on}'\n")
|
|
263
|
+
return 1
|
|
264
|
+
|
|
265
|
+
sys.stdout.write(f"\nPassed: verdict '{overall}' is below threshold '{args.fail_on}'\n")
|
|
266
|
+
return 0
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
if __name__ == "__main__":
|
|
270
|
+
sys.exit(main())
|