@openhands/extensions 0.1.0 → 0.2.0
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/.agents/skills/custom-codereview-guide.md +25 -0
- package/.github/pull_request_template.md +38 -0
- package/.github/release.yml +14 -0
- package/.github/workflows/check-extensions.yml +72 -0
- package/.github/workflows/npm-publish.yml +89 -0
- package/.github/workflows/pr.yml +30 -0
- package/.github/workflows/release.yml +24 -0
- package/.github/workflows/tests.yml +25 -0
- package/.github/workflows/vulnerability-scan.yml +87 -0
- package/.release-please-manifest.json +3 -0
- package/AGENTS.md +132 -0
- package/README.md +10 -0
- package/analysis_results.md +162 -0
- package/marketplaces/large-codebase.json +66 -0
- package/marketplaces/openhands-extensions.json +682 -0
- package/package.json +4 -10
- package/plugins/README.md +30 -0
- package/plugins/city-weather/.plugin/plugin.json +13 -0
- package/plugins/city-weather/README.md +145 -0
- package/plugins/city-weather/commands/now.md +56 -0
- package/plugins/cobol-modernization/.plugin/plugin.json +19 -0
- package/plugins/cobol-modernization/README.md +201 -0
- package/plugins/cobol-modernization/references/troubleshooting.md +18 -0
- package/plugins/cobol-modernization/skills/build-setup/SKILL.md +78 -0
- package/plugins/cobol-modernization/skills/build-setup/scripts/install-gnucobol.sh +32 -0
- package/plugins/cobol-modernization/skills/cobol-modernization-overview/SKILL.md +113 -0
- package/plugins/cobol-modernization/skills/mainfraime-removal/SKILL.md +62 -0
- package/plugins/cobol-modernization/skills/mainfraime-removal/references/cics-transformation-examples.md +45 -0
- package/plugins/cobol-modernization/skills/mainframe-planning/SKILL.md +78 -0
- package/plugins/cobol-modernization/skills/to-java-migration/SKILL.md +59 -0
- package/plugins/cobol-modernization/skills/to-java-migration/references/cobol-to-java-example.md +58 -0
- package/plugins/cobol-modernization/skills/to-java-migration/references/datatype-mappings.md +19 -0
- package/plugins/issue-duplicate-checker/.plugin/plugin.json +13 -0
- package/plugins/issue-duplicate-checker/README.md +51 -0
- package/plugins/issue-duplicate-checker/action.yml +349 -0
- package/plugins/issue-duplicate-checker/scripts/auto_close_duplicate_issues.py +569 -0
- package/plugins/issue-duplicate-checker/scripts/issue_duplicate_check_openhands.py +681 -0
- package/plugins/issue-duplicate-checker/scripts/post_duplicate_notice.js +220 -0
- package/plugins/issue-duplicate-checker/scripts/remove_duplicate_candidate_label.js +27 -0
- package/plugins/magic-test/.plugin/plugin.json +13 -0
- package/plugins/magic-test/skills/magic-word/SKILL.md +33 -0
- package/plugins/migration-scoring/.plugin/plugin.json +19 -0
- package/plugins/migration-scoring/README.md +244 -0
- package/plugins/migration-scoring/skills/migration-mapping/SKILL.md +72 -0
- package/plugins/migration-scoring/skills/migration-report/SKILL.md +118 -0
- package/plugins/migration-scoring/skills/migration-scoring-overview/SKILL.md +126 -0
- package/plugins/migration-scoring/skills/score-quality/SKILL.md +54 -0
- package/plugins/migration-scoring/skills/score-quality/references/scoring-criteria.md +30 -0
- package/plugins/migration-scoring/skills/score-style/SKILL.md +106 -0
- package/plugins/onboarding/.plugin/plugin.json +20 -0
- package/plugins/onboarding/README.md +30 -0
- package/plugins/onboarding/references/criteria.md +144 -0
- package/plugins/onboarding/skills/agent-readiness-report/README.md +23 -0
- package/plugins/onboarding/skills/agent-readiness-report/SKILL.md +122 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_agent_instructions.sh +88 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_build_env.sh +114 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_feedback_loops.sh +133 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_policy.sh +113 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_workflows.sh +127 -0
- package/plugins/onboarding/skills/improve-agent-readiness/README.md +19 -0
- package/plugins/onboarding/skills/improve-agent-readiness/SKILL.md +167 -0
- package/plugins/onboarding/skills/setup-agents-md/README.md +15 -0
- package/plugins/onboarding/skills/setup-agents-md/SKILL.md +150 -0
- package/plugins/onboarding/skills/setup-openhands/README.md +20 -0
- package/plugins/onboarding/skills/setup-openhands/SKILL.md +56 -0
- package/plugins/onboarding/skills/setup-pr-review/README.md +23 -0
- package/plugins/onboarding/skills/setup-pr-review/SKILL.md +72 -0
- package/plugins/openhands/.plugin/plugin.json +13 -0
- package/plugins/openhands/README.md +52 -0
- package/plugins/openhands/SKILL.md +61 -0
- package/plugins/openhands/commands/create.md +55 -0
- package/plugins/openhands/commands/openhands-cloud.md +8 -0
- package/plugins/openhands/scripts/run.sh +69 -0
- package/plugins/pr-review/.plugin/plugin.json +13 -0
- package/plugins/pr-review/README.md +393 -0
- package/plugins/pr-review/action.yml +298 -0
- package/plugins/pr-review/scripts/agent_script.py +1282 -0
- package/plugins/pr-review/scripts/evaluate_review.py +655 -0
- package/plugins/pr-review/scripts/prompt.py +260 -0
- package/plugins/pr-review/workflows/pr-review-by-openhands.yml +51 -0
- package/plugins/pr-review/workflows/pr-review-evaluation.yml +85 -0
- package/plugins/qa-changes/.plugin/plugin.json +11 -0
- package/plugins/qa-changes/README.md +185 -0
- package/plugins/qa-changes/action.yml +181 -0
- package/plugins/qa-changes/scripts/agent_script.py +406 -0
- package/plugins/qa-changes/scripts/evaluate_qa_changes.py +385 -0
- package/plugins/qa-changes/scripts/prompt.py +174 -0
- package/plugins/qa-changes/workflows/qa-changes-by-openhands.yml +50 -0
- package/plugins/qa-changes/workflows/qa-changes-evaluation.yml +85 -0
- package/plugins/release-notes/.plugin/plugin.json +19 -0
- package/plugins/release-notes/README.md +283 -0
- package/plugins/release-notes/SKILL.md +83 -0
- package/plugins/release-notes/action.yml +117 -0
- package/plugins/release-notes/commands/release-notes.md +8 -0
- package/plugins/release-notes/scripts/agent_script.py +292 -0
- package/plugins/release-notes/scripts/generate_release_notes.py +733 -0
- package/plugins/release-notes/scripts/prompt.py +90 -0
- package/plugins/release-notes/scripts/validate_release_notes.py +328 -0
- package/plugins/release-notes/workflows/release-notes.yml +76 -0
- package/plugins/vulnerability-remediation/.plugin/plugin.json +19 -0
- package/plugins/vulnerability-remediation/README.md +217 -0
- package/plugins/vulnerability-remediation/action.yml +187 -0
- package/plugins/vulnerability-remediation/scripts/scan_and_remediate.py +561 -0
- package/plugins/vulnerability-remediation/workflows/vulnerability-scan.yml +87 -0
- package/pyproject.toml +12 -0
- package/release-please-config.json +16 -0
- package/scripts/sync_extensions.py +494 -0
- package/scripts/sync_openhands_sdk_skill.py +264 -0
- package/skills/README.md +159 -0
- package/skills/add-javadoc/.plugin/plugin.json +18 -0
- package/skills/add-javadoc/README.md +40 -0
- package/skills/add-javadoc/SKILL.md +35 -0
- package/skills/add-javadoc/references/example.md +32 -0
- package/skills/add-skill/.plugin/plugin.json +18 -0
- package/skills/add-skill/README.md +67 -0
- package/skills/add-skill/SKILL.md +47 -0
- package/skills/add-skill/scripts/fetch_skill.py +259 -0
- package/skills/agent-creator/.plugin/plugin.json +20 -0
- package/skills/agent-creator/README.md +104 -0
- package/skills/agent-creator/SKILL.md +190 -0
- package/skills/agent-creator/commands/agent-creator.md +8 -0
- package/skills/agent-creator/references/fallback.md +117 -0
- package/skills/agent-memory/.plugin/plugin.json +18 -0
- package/skills/agent-memory/README.md +35 -0
- package/skills/agent-memory/SKILL.md +30 -0
- package/skills/agent-memory/commands/remember.md +8 -0
- package/skills/agent-sdk-builder/.plugin/plugin.json +18 -0
- package/skills/agent-sdk-builder/README.md +40 -0
- package/skills/agent-sdk-builder/SKILL.md +37 -0
- package/skills/agent-sdk-builder/commands/agent-builder.md +8 -0
- package/skills/azure-devops/.plugin/plugin.json +18 -0
- package/skills/azure-devops/README.md +55 -0
- package/skills/azure-devops/SKILL.md +50 -0
- package/skills/bitbucket/.plugin/plugin.json +17 -0
- package/skills/bitbucket/README.md +50 -0
- package/skills/bitbucket/SKILL.md +45 -0
- package/skills/code-review/.plugin/plugin.json +19 -0
- package/skills/code-review/README.md +18 -0
- package/skills/code-review/SKILL.md +208 -0
- package/skills/code-review/commands/codereview-roasted.md +8 -0
- package/skills/code-review/commands/codereview.md +8 -0
- package/skills/code-review/references/risk-evaluation.md +41 -0
- package/skills/code-review/references/supply-chain-security.md +31 -0
- package/skills/code-simplifier/.plugin/plugin.json +21 -0
- package/skills/code-simplifier/README.md +30 -0
- package/skills/code-simplifier/SKILL.md +91 -0
- package/skills/code-simplifier/commands/simplify.md +8 -0
- package/skills/code-simplifier/references/code-quality-review.md +86 -0
- package/skills/code-simplifier/references/code-reuse-review.md +63 -0
- package/skills/code-simplifier/references/efficiency-review.md +81 -0
- package/skills/datadog/.plugin/plugin.json +19 -0
- package/skills/datadog/README.md +100 -0
- package/skills/datadog/SKILL.md +95 -0
- package/skills/deno/.plugin/plugin.json +18 -0
- package/skills/deno/README.md +5 -0
- package/skills/deno/SKILL.md +99 -0
- package/skills/deno/references/README.md +6 -0
- package/skills/discord/.plugin/plugin.json +18 -0
- package/skills/discord/README.md +31 -0
- package/skills/discord/SKILL.md +109 -0
- package/skills/discord/__init__.py +0 -0
- package/skills/discord/references/REFERENCE.md +78 -0
- package/skills/discord/scripts/__init__.py +0 -0
- package/skills/discord/scripts/_http.py +127 -0
- package/skills/discord/scripts/post_webhook.py +106 -0
- package/skills/discord/scripts/send_message.py +102 -0
- package/skills/docker/.plugin/plugin.json +17 -0
- package/skills/docker/README.md +34 -0
- package/skills/docker/SKILL.md +29 -0
- package/skills/evidence-based-citations/.plugin/plugin.json +20 -0
- package/skills/evidence-based-citations/README.md +31 -0
- package/skills/evidence-based-citations/SKILL.md +59 -0
- package/skills/flarglebargle/.plugin/plugin.json +16 -0
- package/skills/flarglebargle/README.md +14 -0
- package/skills/flarglebargle/SKILL.md +9 -0
- package/skills/frontend-design/.plugin/plugin.json +21 -0
- package/skills/frontend-design/LICENSE.txt +177 -0
- package/skills/frontend-design/README.md +42 -0
- package/skills/frontend-design/SKILL.md +42 -0
- package/skills/github/.plugin/plugin.json +19 -0
- package/skills/github/README.md +42 -0
- package/skills/github/SKILL.md +106 -0
- package/skills/github-pr-review/.plugin/plugin.json +18 -0
- package/skills/github-pr-review/README.md +145 -0
- package/skills/github-pr-review/SKILL.md +148 -0
- package/skills/github-pr-review/commands/github-pr-review.md +8 -0
- package/skills/github-pr-reviewer/.plugin/plugin.json +20 -0
- package/skills/github-pr-reviewer/README.md +34 -0
- package/skills/github-pr-reviewer/SKILL.md +89 -0
- package/skills/github-pr-reviewer/commands/pr-reviewer:setup.md +8 -0
- package/skills/github-repo-monitor/.plugin/plugin.json +22 -0
- package/skills/github-repo-monitor/README.md +70 -0
- package/skills/github-repo-monitor/SKILL.md +316 -0
- package/skills/github-repo-monitor/commands/github-monitor:poll.md +8 -0
- package/skills/github-repo-monitor/references/github-api.md +241 -0
- package/skills/github-repo-monitor/references/state-schema.md +160 -0
- package/skills/github-repo-monitor/scripts/main.py +915 -0
- package/skills/github-repo-monitor/tests/test_main.py +400 -0
- package/skills/gitlab/.plugin/plugin.json +17 -0
- package/skills/gitlab/README.md +37 -0
- package/skills/gitlab/SKILL.md +32 -0
- package/skills/incident-retrospective/.plugin/plugin.json +21 -0
- package/skills/incident-retrospective/README.md +34 -0
- package/skills/incident-retrospective/SKILL.md +98 -0
- package/skills/incident-retrospective/commands/incident-retro:setup.md +8 -0
- package/skills/iterate/.plugin/plugin.json +13 -0
- package/skills/iterate/README.md +25 -0
- package/skills/iterate/SKILL.md +399 -0
- package/skills/iterate/commands/babysit.md +8 -0
- package/skills/iterate/commands/iterate.md +8 -0
- package/skills/iterate/commands/verify.md +8 -0
- package/skills/iterate/references/heuristics.md +58 -0
- package/skills/iterate/references/verification.md +96 -0
- package/skills/jupyter/.plugin/plugin.json +18 -0
- package/skills/jupyter/README.md +55 -0
- package/skills/jupyter/SKILL.md +50 -0
- package/skills/kubernetes/.plugin/plugin.json +18 -0
- package/skills/kubernetes/README.md +53 -0
- package/skills/kubernetes/SKILL.md +48 -0
- package/skills/learn-from-code-review/.plugin/plugin.json +19 -0
- package/skills/learn-from-code-review/README.md +64 -0
- package/skills/learn-from-code-review/SKILL.md +186 -0
- package/skills/learn-from-code-review/commands/learn-from-reviews.md +8 -0
- package/skills/linear/.plugin/plugin.json +19 -0
- package/skills/linear/README.md +58 -0
- package/skills/linear/SKILL.md +213 -0
- package/skills/linear-triage/.plugin/plugin.json +21 -0
- package/skills/linear-triage/README.md +34 -0
- package/skills/linear-triage/SKILL.md +91 -0
- package/skills/linear-triage/commands/linear-triage:setup.md +8 -0
- package/skills/notion/.plugin/plugin.json +17 -0
- package/skills/notion/README.md +114 -0
- package/skills/notion/SKILL.md +109 -0
- package/skills/npm/.plugin/plugin.json +17 -0
- package/skills/npm/README.md +14 -0
- package/skills/npm/SKILL.md +9 -0
- package/skills/openhands-api/.plugin/plugin.json +22 -0
- package/skills/openhands-api/README.md +48 -0
- package/skills/openhands-api/SKILL.md +399 -0
- package/skills/openhands-api/references/README.md +33 -0
- package/skills/openhands-api/references/TROUBLESHOOTING.md +81 -0
- package/skills/openhands-api/references/example_prompt.md +12 -0
- package/skills/openhands-api/scripts/openhands_api.py +606 -0
- package/skills/openhands-api/scripts/openhands_api.ts +252 -0
- package/skills/openhands-automation/.plugin/plugin.json +19 -0
- package/skills/openhands-automation/README.md +89 -0
- package/skills/openhands-automation/SKILL.md +875 -0
- package/skills/openhands-automation/commands/automation:create.md +8 -0
- package/skills/openhands-automation/references/ab-testing.md +185 -0
- package/skills/openhands-automation/references/custom-automation.md +644 -0
- package/skills/openhands-sdk/.plugin/plugin.json +20 -0
- package/skills/openhands-sdk/README.md +22 -0
- package/skills/openhands-sdk/SKILL.md +229 -0
- package/skills/openhands-sdk/commands/sdk.md +8 -0
- package/skills/pdflatex/.plugin/plugin.json +18 -0
- package/skills/pdflatex/README.md +39 -0
- package/skills/pdflatex/SKILL.md +34 -0
- package/skills/prd/.plugin/plugin.json +19 -0
- package/skills/prd/README.md +28 -0
- package/skills/prd/SKILL.md +237 -0
- package/skills/prd/commands/prd.md +8 -0
- package/skills/qa-changes/README.md +18 -0
- package/skills/qa-changes/SKILL.md +229 -0
- package/skills/qa-changes/commands/qa-changes.md +8 -0
- package/skills/release-notes/README.md +24 -0
- package/skills/release-notes/SKILL.md +19 -0
- package/skills/release-notes/commands/release-notes.md +8 -0
- package/skills/research-brief/.plugin/plugin.json +20 -0
- package/skills/research-brief/README.md +34 -0
- package/skills/research-brief/SKILL.md +99 -0
- package/skills/research-brief/commands/research-brief:setup.md +8 -0
- package/skills/security/.plugin/plugin.json +18 -0
- package/skills/security/README.md +38 -0
- package/skills/security/SKILL.md +33 -0
- package/skills/skill-creator/.plugin/plugin.json +17 -0
- package/skills/skill-creator/LICENSE.txt +202 -0
- package/skills/skill-creator/README.md +182 -0
- package/skills/skill-creator/SKILL.md +545 -0
- package/skills/skill-creator/references/output-patterns.md +82 -0
- package/skills/skill-creator/references/workflows.md +28 -0
- package/skills/skill-creator/scripts/init_skill.py +303 -0
- package/skills/skill-creator/scripts/quick_validate.py +95 -0
- package/skills/slack-channel-monitor/.plugin/plugin.json +21 -0
- package/skills/slack-channel-monitor/README.md +91 -0
- package/skills/slack-channel-monitor/SKILL.md +276 -0
- package/skills/slack-channel-monitor/commands/slack-monitor:poll.md +8 -0
- package/skills/slack-channel-monitor/references/slack-api.md +207 -0
- package/skills/slack-channel-monitor/references/state-schema.md +180 -0
- package/skills/slack-channel-monitor/scripts/main.py +962 -0
- package/skills/slack-standup-digest/.plugin/plugin.json +21 -0
- package/skills/slack-standup-digest/README.md +34 -0
- package/skills/slack-standup-digest/SKILL.md +92 -0
- package/skills/slack-standup-digest/commands/standup-digest:setup.md +8 -0
- package/skills/spark-version-upgrade/.plugin/plugin.json +20 -0
- package/skills/spark-version-upgrade/README.md +54 -0
- package/skills/spark-version-upgrade/SKILL.md +233 -0
- package/skills/ssh/.plugin/plugin.json +18 -0
- package/skills/ssh/README.md +140 -0
- package/skills/ssh/SKILL.md +135 -0
- package/skills/swift-linux/.plugin/plugin.json +17 -0
- package/skills/swift-linux/README.md +86 -0
- package/skills/swift-linux/SKILL.md +81 -0
- package/skills/theme-factory/.plugin/plugin.json +19 -0
- package/skills/theme-factory/LICENSE.txt +202 -0
- package/skills/theme-factory/README.md +58 -0
- package/skills/theme-factory/SKILL.md +59 -0
- package/skills/theme-factory/theme-showcase.pdf +0 -0
- package/skills/theme-factory/themes/arctic-frost.md +19 -0
- package/skills/theme-factory/themes/botanical-garden.md +19 -0
- package/skills/theme-factory/themes/desert-rose.md +19 -0
- package/skills/theme-factory/themes/forest-canopy.md +19 -0
- package/skills/theme-factory/themes/golden-hour.md +19 -0
- package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
- package/skills/theme-factory/themes/modern-minimalist.md +19 -0
- package/skills/theme-factory/themes/ocean-depths.md +19 -0
- package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
- package/skills/theme-factory/themes/tech-innovation.md +19 -0
- package/skills/uv/.plugin/plugin.json +18 -0
- package/skills/uv/README.md +5 -0
- package/skills/uv/SKILL.md +95 -0
- package/skills/uv/references/README.md +5 -0
- package/skills/vercel/.plugin/plugin.json +18 -0
- package/skills/vercel/README.md +108 -0
- package/skills/vercel/SKILL.md +103 -0
- package/tests/test_add_skill_installs_to_agents_dir.py +42 -0
- package/tests/test_catalogs.py +109 -0
- package/tests/test_code_review_risk_evaluation.py +94 -0
- package/tests/test_issue_duplicate_checker.py +240 -0
- package/tests/test_openhands_api_python.py +152 -0
- package/tests/test_plugin_manifest.py +83 -0
- package/tests/test_pr_review_diff_payload.py +202 -0
- package/tests/test_pr_review_feedback.py +263 -0
- package/tests/test_pr_review_prompt.py +152 -0
- package/tests/test_pr_review_review_context.py +253 -0
- package/tests/test_qa_changes.py +232 -0
- package/tests/test_qa_changes_evaluation.py +259 -0
- package/tests/test_release_notes_generator.py +990 -0
- package/tests/test_sdk_loading.py +150 -0
- package/tests/test_skill_plugin_loading.py +149 -0
- package/tests/test_skills_have_readme.py +66 -0
- package/tests/test_sync_extensions.py +292 -0
- package/tests/test_workflow_sync.py +46 -0
- package/utils/analysis/README.md +7 -0
- package/utils/analysis/laminar_signals/README.md +211 -0
- package/utils/analysis/laminar_signals/analyze.py +780 -0
- package/utils/analysis/laminar_signals/templates/default.j2 +49 -0
- package/utils/analysis/laminar_signals/templates/pr_review.j2 +61 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Vulnerability Scan and Remediation Agent
|
|
4
|
+
|
|
5
|
+
This script runs a Trivy security scan on the repository, identifies vulnerabilities
|
|
6
|
+
above a severity threshold, and uses OpenHands agents to create PRs with fixes.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python scan_and_remediate.py --scan-only # Only run Trivy scan, output results
|
|
10
|
+
python scan_and_remediate.py --remediate # Run remediation on existing scan results
|
|
11
|
+
python scan_and_remediate.py # Full scan + remediation (legacy mode)
|
|
12
|
+
|
|
13
|
+
Environment Variables:
|
|
14
|
+
LLM_API_KEY: API key for the LLM (required for remediation)
|
|
15
|
+
LLM_MODEL: Language model to use (default: anthropic/claude-sonnet-4-5-20250929)
|
|
16
|
+
LLM_BASE_URL: Optional base URL for LLM API
|
|
17
|
+
GITHUB_TOKEN: GitHub token for API access and creating PRs (required for remediation)
|
|
18
|
+
REPO_NAME: Repository name in format owner/repo (required for remediation)
|
|
19
|
+
SEVERITY_THRESHOLD: Minimum severity to remediate (default: HIGH)
|
|
20
|
+
MAX_VULNERABILITIES: Maximum vulnerabilities to fix per run (default: 5, 0=unlimited)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import json
|
|
27
|
+
import logging
|
|
28
|
+
import os
|
|
29
|
+
import subprocess
|
|
30
|
+
import sys
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
# Set up basic logging for scan-only mode (no openhands dependency)
|
|
35
|
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
|
36
|
+
basic_logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
SEVERITY_ORDER = ["CRITICAL", "HIGH", "MEDIUM", "LOW"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Vulnerability:
|
|
43
|
+
"""Represents a security vulnerability."""
|
|
44
|
+
|
|
45
|
+
vuln_id: str
|
|
46
|
+
package_name: str
|
|
47
|
+
installed_version: str
|
|
48
|
+
fixed_version: str | None
|
|
49
|
+
severity: str
|
|
50
|
+
title: str
|
|
51
|
+
description: str
|
|
52
|
+
target: str
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def has_fix(self) -> bool:
|
|
56
|
+
return self.fixed_version is not None and self.fixed_version != ""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_required_env(name: str) -> str:
|
|
60
|
+
"""Get a required environment variable."""
|
|
61
|
+
value = os.getenv(name)
|
|
62
|
+
if not value:
|
|
63
|
+
raise ValueError(f"{name} environment variable is required")
|
|
64
|
+
return value
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def run_trivy_scan(repo_path: str, logger) -> dict:
|
|
68
|
+
"""Run Trivy scan on the repository and return results."""
|
|
69
|
+
logger.info("Running Trivy security scan...")
|
|
70
|
+
|
|
71
|
+
output_file = Path(repo_path) / "trivy-results.json"
|
|
72
|
+
|
|
73
|
+
cmd = [
|
|
74
|
+
"trivy",
|
|
75
|
+
"fs",
|
|
76
|
+
"--format",
|
|
77
|
+
"json",
|
|
78
|
+
"--output",
|
|
79
|
+
str(output_file),
|
|
80
|
+
"--severity",
|
|
81
|
+
"CRITICAL,HIGH,MEDIUM,LOW",
|
|
82
|
+
repo_path,
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
|
|
87
|
+
if result.returncode != 0 and not output_file.exists():
|
|
88
|
+
logger.error(f"Trivy scan failed: {result.stderr}")
|
|
89
|
+
raise RuntimeError(f"Trivy scan failed: {result.stderr}")
|
|
90
|
+
|
|
91
|
+
with open(output_file) as f:
|
|
92
|
+
return json.load(f)
|
|
93
|
+
|
|
94
|
+
except subprocess.TimeoutExpired:
|
|
95
|
+
raise RuntimeError("Trivy scan timed out after 10 minutes")
|
|
96
|
+
except json.JSONDecodeError as e:
|
|
97
|
+
raise RuntimeError(f"Failed to parse Trivy output: {e}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def parse_vulnerabilities(trivy_output: dict) -> list[Vulnerability]:
|
|
101
|
+
"""Parse Trivy output into Vulnerability objects."""
|
|
102
|
+
vulnerabilities = []
|
|
103
|
+
|
|
104
|
+
results = trivy_output.get("Results", [])
|
|
105
|
+
for result in results:
|
|
106
|
+
target = result.get("Target", "unknown")
|
|
107
|
+
vulns = result.get("Vulnerabilities", [])
|
|
108
|
+
|
|
109
|
+
for vuln in vulns:
|
|
110
|
+
vulnerabilities.append(
|
|
111
|
+
Vulnerability(
|
|
112
|
+
vuln_id=vuln.get("VulnerabilityID", ""),
|
|
113
|
+
package_name=vuln.get("PkgName", ""),
|
|
114
|
+
installed_version=vuln.get("InstalledVersion", ""),
|
|
115
|
+
fixed_version=vuln.get("FixedVersion"),
|
|
116
|
+
severity=vuln.get("Severity", "UNKNOWN"),
|
|
117
|
+
title=vuln.get("Title", ""),
|
|
118
|
+
description=vuln.get("Description", ""),
|
|
119
|
+
target=target,
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return vulnerabilities
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def filter_vulnerabilities(
|
|
127
|
+
vulnerabilities: list[Vulnerability],
|
|
128
|
+
severity_threshold: str,
|
|
129
|
+
max_count: int,
|
|
130
|
+
) -> list[Vulnerability]:
|
|
131
|
+
"""Filter vulnerabilities by severity threshold and limit count.
|
|
132
|
+
|
|
133
|
+
Note: severity_threshold should be validated before calling this function.
|
|
134
|
+
"""
|
|
135
|
+
# Ensure valid threshold (defensive, validation should happen earlier)
|
|
136
|
+
if severity_threshold not in SEVERITY_ORDER:
|
|
137
|
+
severity_threshold = "HIGH"
|
|
138
|
+
|
|
139
|
+
threshold_index = SEVERITY_ORDER.index(severity_threshold)
|
|
140
|
+
allowed_severities = set(SEVERITY_ORDER[: threshold_index + 1])
|
|
141
|
+
|
|
142
|
+
# Filter by severity and only include those with fixes
|
|
143
|
+
filtered = [
|
|
144
|
+
v
|
|
145
|
+
for v in vulnerabilities
|
|
146
|
+
if v.severity in allowed_severities and v.has_fix
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
# Sort by severity (most critical first)
|
|
150
|
+
filtered.sort(key=lambda v: SEVERITY_ORDER.index(v.severity))
|
|
151
|
+
|
|
152
|
+
# Limit count
|
|
153
|
+
if max_count > 0:
|
|
154
|
+
filtered = filtered[:max_count]
|
|
155
|
+
|
|
156
|
+
return filtered
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def create_remediation_prompt(vuln: Vulnerability, repo_name: str) -> str:
|
|
160
|
+
"""Create the prompt for the remediation agent."""
|
|
161
|
+
return f"""You are a security engineer tasked with fixing a vulnerability in a repository.
|
|
162
|
+
|
|
163
|
+
## Repository
|
|
164
|
+
{repo_name}
|
|
165
|
+
|
|
166
|
+
## Vulnerability Details
|
|
167
|
+
- **ID**: {vuln.vuln_id}
|
|
168
|
+
- **Package**: {vuln.package_name}
|
|
169
|
+
- **Severity**: {vuln.severity}
|
|
170
|
+
- **Current Version**: {vuln.installed_version}
|
|
171
|
+
- **Fixed Version**: {vuln.fixed_version}
|
|
172
|
+
- **Location**: {vuln.target}
|
|
173
|
+
- **Title**: {vuln.title}
|
|
174
|
+
- **Description**: {vuln.description}
|
|
175
|
+
|
|
176
|
+
## Your Task
|
|
177
|
+
|
|
178
|
+
1. **Analyze** the vulnerability and understand what needs to be fixed
|
|
179
|
+
|
|
180
|
+
2. **Find ALL dependency files across the repository**
|
|
181
|
+
**CRITICAL**: Search the repository for ALL dependency files that contain the vulnerable package. Do NOT assume dependencies only exist in the root directory.
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
# Find ALL dependency files containing the package (excludes dependency/build directories)
|
|
185
|
+
find . \( -name node_modules -o -name .venv -o -name venv -o -name vendor -o -name .git -o -name __pycache__ -o -name dist -o -name build \) -prune -o -name "pyproject.toml" -exec grep -l "{vuln.package_name}" {} + 2>/dev/null
|
|
186
|
+
find . \( -name node_modules -o -name .venv -o -name venv -o -name vendor -o -name .git \) -prune -o -name "requirements*.txt" -exec grep -l "{vuln.package_name}" {} + 2>/dev/null
|
|
187
|
+
find . \( -name node_modules -o -name .venv -o -name venv -o -name vendor -o -name .git \) -prune -o -name "package.json" -exec grep -l "{vuln.package_name}" {} + 2>/dev/null
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Update {vuln.package_name} from {vuln.installed_version} to {vuln.fixed_version} in **EVERY** file found.
|
|
191
|
+
|
|
192
|
+
3. **CRITICAL: Sync/regenerate ALL lockfiles in the repository**
|
|
193
|
+
After updating the version in ALL manifest files, you MUST regenerate ALL corresponding lockfiles. Do NOT manually edit lockfiles.
|
|
194
|
+
|
|
195
|
+
**CRITICAL**: First, find ALL lockfiles (excluding dependency/build directories):
|
|
196
|
+
```bash
|
|
197
|
+
find . \\( -name node_modules -o -name .venv -o -name venv -o -name vendor -o -name .git \\) -prune -o \\( -name "poetry.lock" -o -name "uv.lock" -o -name "package-lock.json" -o -name "yarn.lock" -o -name "pnpm-lock.yaml" -o -name "Cargo.lock" -o -name "go.sum" \\) -print
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
You MUST regenerate EVERY lockfile found, not just those in the root directory.
|
|
201
|
+
|
|
202
|
+
**IMPORTANT**: To avoid unnecessary diff noise, you MUST detect and use the same tool version that originally generated each lockfile. Each lockfile contains a version header that indicates which tool version was used.
|
|
203
|
+
|
|
204
|
+
**Note**: This agent runs in an isolated execution environment (container), so installing specific tool versions with `--force` will not affect other projects or system-wide installations.
|
|
205
|
+
|
|
206
|
+
For EACH lockfile found, `cd` to its directory and regenerate it:
|
|
207
|
+
|
|
208
|
+
- **Poetry (pyproject.toml + poetry.lock)**:
|
|
209
|
+
1. `cd` to the directory containing the lockfile
|
|
210
|
+
2. Extract version: `grep -m1 "^# This file is automatically @generated by Poetry" poetry.lock | sed 's/.*Poetry \\([0-9.]*\\).*/\\1/'`
|
|
211
|
+
3. If a version is found, install it: `pipx install poetry==$POETRY_VERSION --force`
|
|
212
|
+
4. Verify installation: `poetry --version | grep "$POETRY_VERSION"` (proceed only if successful)
|
|
213
|
+
5. If version extraction fails or returns empty, proceed with the currently installed version and note this in your output
|
|
214
|
+
6. Run: `poetry lock --no-update` or `poetry update {vuln.package_name}`
|
|
215
|
+
|
|
216
|
+
- **uv (pyproject.toml + uv.lock)**:
|
|
217
|
+
1. `cd` to the directory containing the lockfile
|
|
218
|
+
2. Extract version: `grep -m1 "^# This file was autogenerated by uv" uv.lock | sed 's/.*uv version \\([0-9.]*\\).*/\\1/'`
|
|
219
|
+
3. If a version is found, install it: `pipx install uv==$UV_VERSION --force`
|
|
220
|
+
4. Verify installation: `uv --version | grep "$UV_VERSION"` (proceed only if successful)
|
|
221
|
+
5. If version extraction fails or returns empty, proceed with the currently installed version and note this in your output
|
|
222
|
+
6. Run: `uv lock --upgrade-package {vuln.package_name}` or `uv sync`
|
|
223
|
+
|
|
224
|
+
- **npm (package.json + package-lock.json)**: `cd` to directory, run `npm install` or `npm update {vuln.package_name}`
|
|
225
|
+
- **yarn (package.json + yarn.lock)**: `cd` to directory, run `yarn install` or `yarn upgrade {vuln.package_name}`
|
|
226
|
+
- **pnpm (package.json + pnpm-lock.yaml)**: `cd` to directory, run `pnpm install` or `pnpm update {vuln.package_name}`
|
|
227
|
+
- **pip (requirements.txt)**: Update the version directly in requirements.txt
|
|
228
|
+
- **Go (go.mod + go.sum)**: `cd` to directory, run `go mod tidy`
|
|
229
|
+
- **Cargo (Cargo.toml + Cargo.lock)**: `cd` to directory, run `cargo update -p {vuln.package_name}`
|
|
230
|
+
- **Maven (pom.xml)**: Update the version directly in pom.xml
|
|
231
|
+
- **Gradle**: Update the version in build.gradle/build.gradle.kts
|
|
232
|
+
|
|
233
|
+
4. **Verify** the change doesn't break the build (run any available build/test commands)
|
|
234
|
+
5. **Create a branch** named `fix/{vuln.vuln_id.lower()}`
|
|
235
|
+
6. **Commit** your changes with a clear message explaining the security fix
|
|
236
|
+
7. **Push** the branch to origin
|
|
237
|
+
8. **Create a Pull Request** using the GitHub CLI:
|
|
238
|
+
```bash
|
|
239
|
+
gh pr create --title "fix: {vuln.vuln_id} - Update {vuln.package_name} to {vuln.fixed_version}" \\
|
|
240
|
+
--body "## Security Fix
|
|
241
|
+
|
|
242
|
+
This PR addresses {vuln.vuln_id} ({vuln.severity} severity).
|
|
243
|
+
|
|
244
|
+
### Vulnerability
|
|
245
|
+
{vuln.title}
|
|
246
|
+
|
|
247
|
+
### Changes
|
|
248
|
+
- Updated `{vuln.package_name}` from `{vuln.installed_version}` to `{vuln.fixed_version}`
|
|
249
|
+
|
|
250
|
+
### References
|
|
251
|
+
- https://nvd.nist.gov/vuln/detail/{vuln.vuln_id}
|
|
252
|
+
"
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Important Notes
|
|
256
|
+
- Do NOT modify any code beyond what's necessary for the fix
|
|
257
|
+
- Do NOT manually edit lockfiles - always use the package manager commands above
|
|
258
|
+
- If the package update requires other dependency changes, include them
|
|
259
|
+
- If you encounter conflicts or issues, document them in the PR description
|
|
260
|
+
- Always test that the fix doesn't break the build before creating the PR
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def create_agent(config: dict, logger):
|
|
265
|
+
"""Create and configure the remediation agent."""
|
|
266
|
+
from openhands.sdk import LLM, Agent
|
|
267
|
+
from openhands.tools.preset.default import get_default_condenser, get_default_tools
|
|
268
|
+
|
|
269
|
+
llm_config = {
|
|
270
|
+
"model": config["model"],
|
|
271
|
+
"api_key": config["api_key"],
|
|
272
|
+
"usage_id": "vulnerability_remediation",
|
|
273
|
+
"drop_params": True,
|
|
274
|
+
}
|
|
275
|
+
if config.get("base_url"):
|
|
276
|
+
llm_config["base_url"] = config["base_url"]
|
|
277
|
+
|
|
278
|
+
llm = LLM(**llm_config)
|
|
279
|
+
|
|
280
|
+
return Agent(
|
|
281
|
+
llm=llm,
|
|
282
|
+
tools=get_default_tools(enable_browser=False),
|
|
283
|
+
system_prompt_kwargs={"cli_mode": True},
|
|
284
|
+
condenser=get_default_condenser(
|
|
285
|
+
llm=llm.model_copy(update={"usage_id": "condenser"})
|
|
286
|
+
),
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def remediate_vulnerability(
|
|
291
|
+
agent,
|
|
292
|
+
vuln: Vulnerability,
|
|
293
|
+
repo_name: str,
|
|
294
|
+
secrets: dict[str, str],
|
|
295
|
+
logger,
|
|
296
|
+
) -> dict:
|
|
297
|
+
"""Run the remediation agent for a single vulnerability."""
|
|
298
|
+
from openhands.sdk import Conversation
|
|
299
|
+
|
|
300
|
+
logger.info(f"Remediating {vuln.vuln_id} ({vuln.severity}): {vuln.package_name}")
|
|
301
|
+
|
|
302
|
+
prompt = create_remediation_prompt(vuln, repo_name)
|
|
303
|
+
cwd = os.getcwd()
|
|
304
|
+
|
|
305
|
+
conversation = Conversation(
|
|
306
|
+
agent=agent,
|
|
307
|
+
workspace=cwd,
|
|
308
|
+
secrets=secrets,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
conversation.send_message(prompt)
|
|
313
|
+
conversation.run()
|
|
314
|
+
|
|
315
|
+
metrics = conversation.conversation_stats.get_combined_metrics()
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
"vuln_id": vuln.vuln_id,
|
|
319
|
+
"package": vuln.package_name,
|
|
320
|
+
"severity": vuln.severity,
|
|
321
|
+
"status": "completed",
|
|
322
|
+
"cost": metrics.accumulated_cost,
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
except Exception as e:
|
|
326
|
+
logger.error(f"Failed to remediate {vuln.vuln_id}: {type(e).__name__}: {e}")
|
|
327
|
+
return {
|
|
328
|
+
"vuln_id": vuln.vuln_id,
|
|
329
|
+
"package": vuln.package_name,
|
|
330
|
+
"severity": vuln.severity,
|
|
331
|
+
"status": "failed",
|
|
332
|
+
"error": str(e),
|
|
333
|
+
"error_type": type(e).__name__,
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def save_report(vulnerabilities: list[Vulnerability], results: list[dict], logger) -> None:
|
|
338
|
+
"""Save the remediation report."""
|
|
339
|
+
report = {
|
|
340
|
+
"total_vulnerabilities_found": len(vulnerabilities),
|
|
341
|
+
"remediation_results": results,
|
|
342
|
+
"summary": {
|
|
343
|
+
"completed": len([r for r in results if r["status"] == "completed"]),
|
|
344
|
+
"failed": len([r for r in results if r["status"] == "failed"]),
|
|
345
|
+
"total_cost": sum(r.get("cost", 0) for r in results),
|
|
346
|
+
},
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
with open("remediation-report.json", "w") as f:
|
|
350
|
+
json.dump(report, f, indent=2)
|
|
351
|
+
|
|
352
|
+
logger.info("Remediation report saved to remediation-report.json")
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def save_scan_results(
|
|
356
|
+
all_vulns: list[Vulnerability],
|
|
357
|
+
vulns_to_fix: list[Vulnerability],
|
|
358
|
+
severity_threshold: str,
|
|
359
|
+
logger,
|
|
360
|
+
) -> None:
|
|
361
|
+
"""Save scan results for the action to read."""
|
|
362
|
+
scan_results = {
|
|
363
|
+
"total_vulnerabilities": len(all_vulns),
|
|
364
|
+
"vulnerabilities_to_fix": len(vulns_to_fix),
|
|
365
|
+
"severity_threshold": severity_threshold,
|
|
366
|
+
"vulnerabilities": [
|
|
367
|
+
{
|
|
368
|
+
"vuln_id": v.vuln_id,
|
|
369
|
+
"package_name": v.package_name,
|
|
370
|
+
"installed_version": v.installed_version,
|
|
371
|
+
"fixed_version": v.fixed_version,
|
|
372
|
+
"severity": v.severity,
|
|
373
|
+
"title": v.title,
|
|
374
|
+
"target": v.target,
|
|
375
|
+
}
|
|
376
|
+
for v in vulns_to_fix
|
|
377
|
+
],
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
with open("scan-results.json", "w") as f:
|
|
381
|
+
json.dump(scan_results, f, indent=2)
|
|
382
|
+
|
|
383
|
+
logger.info("Scan results saved to scan-results.json")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def validate_severity_threshold(severity_threshold: str, logger) -> str:
|
|
387
|
+
"""Validate severity threshold, warn and default to HIGH if invalid."""
|
|
388
|
+
if severity_threshold not in SEVERITY_ORDER:
|
|
389
|
+
logger.warning(
|
|
390
|
+
f"Invalid severity threshold '{severity_threshold}'. "
|
|
391
|
+
f"Valid options: {', '.join(SEVERITY_ORDER)}. Defaulting to 'HIGH'."
|
|
392
|
+
)
|
|
393
|
+
return "HIGH"
|
|
394
|
+
return severity_threshold
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def run_scan_only():
|
|
398
|
+
"""Run only the Trivy scan and save results (no OpenHands dependency)."""
|
|
399
|
+
logger = basic_logger
|
|
400
|
+
logger.info("Running vulnerability scan (scan-only mode)...")
|
|
401
|
+
|
|
402
|
+
severity_threshold = os.getenv("SEVERITY_THRESHOLD", "HIGH")
|
|
403
|
+
severity_threshold = validate_severity_threshold(severity_threshold, logger)
|
|
404
|
+
max_vulns = int(os.getenv("MAX_VULNERABILITIES", "5"))
|
|
405
|
+
|
|
406
|
+
logger.info(f"Severity threshold: {severity_threshold}")
|
|
407
|
+
logger.info(f"Max vulnerabilities: {max_vulns if max_vulns > 0 else 'unlimited'}")
|
|
408
|
+
|
|
409
|
+
# Run Trivy scan
|
|
410
|
+
repo_path = os.getcwd()
|
|
411
|
+
trivy_output = run_trivy_scan(repo_path, logger)
|
|
412
|
+
|
|
413
|
+
# Parse and filter vulnerabilities
|
|
414
|
+
all_vulns = parse_vulnerabilities(trivy_output)
|
|
415
|
+
logger.info(f"Found {len(all_vulns)} total vulnerabilities")
|
|
416
|
+
|
|
417
|
+
vulns_to_fix = filter_vulnerabilities(all_vulns, severity_threshold, max_vulns)
|
|
418
|
+
logger.info(
|
|
419
|
+
f"Filtered to {len(vulns_to_fix)} vulnerabilities "
|
|
420
|
+
f"({severity_threshold}+ with available fixes)"
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
# Save scan results for the action to read
|
|
424
|
+
save_scan_results(all_vulns, vulns_to_fix, severity_threshold, logger)
|
|
425
|
+
|
|
426
|
+
print(f"\n=== Vulnerability Scan Summary ===")
|
|
427
|
+
print(f"Total Vulnerabilities Found: {len(all_vulns)}")
|
|
428
|
+
print(f"Vulnerabilities to Remediate: {len(vulns_to_fix)}")
|
|
429
|
+
|
|
430
|
+
if not vulns_to_fix:
|
|
431
|
+
logger.info("✅ No vulnerabilities found that need remediation!")
|
|
432
|
+
else:
|
|
433
|
+
logger.info(f"🔍 Found {len(vulns_to_fix)} vulnerabilities to remediate")
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def run_remediation():
|
|
437
|
+
"""Run remediation using existing scan results (requires OpenHands)."""
|
|
438
|
+
from openhands.sdk import LLM, Agent, Conversation, get_logger
|
|
439
|
+
from openhands.tools.preset.default import get_default_condenser, get_default_tools
|
|
440
|
+
|
|
441
|
+
logger = get_logger(__name__)
|
|
442
|
+
logger.info("Running vulnerability remediation...")
|
|
443
|
+
|
|
444
|
+
# Load scan results
|
|
445
|
+
scan_results_file = Path("scan-results.json")
|
|
446
|
+
if not scan_results_file.exists():
|
|
447
|
+
logger.error("No scan results found. Run with --scan-only first.")
|
|
448
|
+
sys.exit(1)
|
|
449
|
+
|
|
450
|
+
with open(scan_results_file) as f:
|
|
451
|
+
scan_results = json.load(f)
|
|
452
|
+
|
|
453
|
+
vulns_data = scan_results.get("vulnerabilities", [])
|
|
454
|
+
if not vulns_data:
|
|
455
|
+
logger.info("No vulnerabilities to remediate!")
|
|
456
|
+
return
|
|
457
|
+
|
|
458
|
+
# Convert to Vulnerability objects
|
|
459
|
+
vulns_to_fix = [
|
|
460
|
+
Vulnerability(
|
|
461
|
+
vuln_id=v["vuln_id"],
|
|
462
|
+
package_name=v["package_name"],
|
|
463
|
+
installed_version=v["installed_version"],
|
|
464
|
+
fixed_version=v["fixed_version"],
|
|
465
|
+
severity=v["severity"],
|
|
466
|
+
title=v["title"],
|
|
467
|
+
description="", # Not stored in scan results
|
|
468
|
+
target=v["target"],
|
|
469
|
+
)
|
|
470
|
+
for v in vulns_data
|
|
471
|
+
]
|
|
472
|
+
|
|
473
|
+
# Get configuration
|
|
474
|
+
config = {
|
|
475
|
+
"api_key": get_required_env("LLM_API_KEY"),
|
|
476
|
+
"model": os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929"),
|
|
477
|
+
"base_url": os.getenv("LLM_BASE_URL"),
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
github_token = get_required_env("GITHUB_TOKEN")
|
|
481
|
+
repo_name = get_required_env("REPO_NAME")
|
|
482
|
+
|
|
483
|
+
logger.info(f"Repository: {repo_name}")
|
|
484
|
+
logger.info(f"Remediating {len(vulns_to_fix)} vulnerabilities...")
|
|
485
|
+
|
|
486
|
+
# Create agent and remediate
|
|
487
|
+
agent = create_agent(config, logger)
|
|
488
|
+
secrets = {
|
|
489
|
+
"LLM_API_KEY": config["api_key"],
|
|
490
|
+
"GITHUB_TOKEN": github_token,
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
results = []
|
|
494
|
+
for vuln in vulns_to_fix:
|
|
495
|
+
result = remediate_vulnerability(agent, vuln, repo_name, secrets, logger)
|
|
496
|
+
results.append(result)
|
|
497
|
+
|
|
498
|
+
# Save report
|
|
499
|
+
save_report(vulns_to_fix, results, logger)
|
|
500
|
+
|
|
501
|
+
# Print summary
|
|
502
|
+
completed = len([r for r in results if r["status"] == "completed"])
|
|
503
|
+
failed = len([r for r in results if r["status"] == "failed"])
|
|
504
|
+
total_cost = sum(r.get("cost", 0) for r in results)
|
|
505
|
+
|
|
506
|
+
print("\n=== Vulnerability Remediation Summary ===")
|
|
507
|
+
print(f"Attempted Remediations: {len(results)}")
|
|
508
|
+
print(f"Completed: {completed}")
|
|
509
|
+
print(f"Failed: {failed}")
|
|
510
|
+
print(f"Total Cost: ${total_cost:.6f}")
|
|
511
|
+
|
|
512
|
+
if failed > 0:
|
|
513
|
+
logger.warning(f"{failed} remediations failed - check logs for details")
|
|
514
|
+
sys.exit(1)
|
|
515
|
+
|
|
516
|
+
logger.info("Vulnerability remediation completed successfully")
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def main():
|
|
520
|
+
"""Run vulnerability scan and remediation (legacy mode - full workflow).
|
|
521
|
+
|
|
522
|
+
This combines scan-only and remediate modes for backward compatibility.
|
|
523
|
+
Prefer using --scan-only and --remediate separately for better control.
|
|
524
|
+
"""
|
|
525
|
+
# Run scan first
|
|
526
|
+
run_scan_only()
|
|
527
|
+
|
|
528
|
+
# Check if there are vulnerabilities to fix
|
|
529
|
+
scan_results_file = Path("scan-results.json")
|
|
530
|
+
if scan_results_file.exists():
|
|
531
|
+
with open(scan_results_file) as f:
|
|
532
|
+
scan_results = json.load(f)
|
|
533
|
+
if scan_results.get("vulnerabilities_to_fix", 0) > 0:
|
|
534
|
+
# Run remediation
|
|
535
|
+
run_remediation()
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
if __name__ == "__main__":
|
|
539
|
+
parser = argparse.ArgumentParser(
|
|
540
|
+
description="Vulnerability Scan and Remediation Agent"
|
|
541
|
+
)
|
|
542
|
+
parser.add_argument(
|
|
543
|
+
"--scan-only",
|
|
544
|
+
action="store_true",
|
|
545
|
+
help="Only run Trivy scan, save results without starting agent",
|
|
546
|
+
)
|
|
547
|
+
parser.add_argument(
|
|
548
|
+
"--remediate",
|
|
549
|
+
action="store_true",
|
|
550
|
+
help="Run remediation using existing scan results",
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
args = parser.parse_args()
|
|
554
|
+
|
|
555
|
+
if args.scan_only:
|
|
556
|
+
run_scan_only()
|
|
557
|
+
elif args.remediate:
|
|
558
|
+
run_remediation()
|
|
559
|
+
else:
|
|
560
|
+
# Legacy mode: full scan + remediation
|
|
561
|
+
main()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Vulnerability Scan and Remediation Workflow
|
|
2
|
+
#
|
|
3
|
+
# This is a thin wrapper that uses the OpenHands vulnerability-remediation action.
|
|
4
|
+
# It auto-updates when the action in OpenHands/extensions is updated.
|
|
5
|
+
#
|
|
6
|
+
# INSTALLATION:
|
|
7
|
+
# curl -o .github/workflows/vulnerability-scan.yml \
|
|
8
|
+
# https://raw.githubusercontent.com/OpenHands/extensions/main/plugins/vulnerability-remediation/workflows/vulnerability-scan.yml
|
|
9
|
+
#
|
|
10
|
+
# REQUIRED SECRETS:
|
|
11
|
+
# - LLM_API_KEY: API key for your LLM provider (OpenAI, Anthropic, etc.)
|
|
12
|
+
#
|
|
13
|
+
# OPTIONAL SECRETS:
|
|
14
|
+
# - PAT_TOKEN: GitHub PAT for creating PRs (uses GITHUB_TOKEN if not set)
|
|
15
|
+
#
|
|
16
|
+
# The action will:
|
|
17
|
+
# 1. Run a Trivy security scan
|
|
18
|
+
# 2. Skip agent if no vulnerabilities found (saves costs)
|
|
19
|
+
# 3. Create PRs for fixable vulnerabilities
|
|
20
|
+
|
|
21
|
+
name: Vulnerability Scan and Remediation
|
|
22
|
+
|
|
23
|
+
on:
|
|
24
|
+
# Run weekly on Monday at 9am UTC
|
|
25
|
+
schedule:
|
|
26
|
+
- cron: '0 9 * * 1'
|
|
27
|
+
|
|
28
|
+
# Allow manual trigger
|
|
29
|
+
workflow_dispatch:
|
|
30
|
+
inputs:
|
|
31
|
+
severity_threshold:
|
|
32
|
+
description: 'Minimum severity to remediate'
|
|
33
|
+
required: false
|
|
34
|
+
default: 'HIGH'
|
|
35
|
+
type: choice
|
|
36
|
+
options:
|
|
37
|
+
- CRITICAL
|
|
38
|
+
- HIGH
|
|
39
|
+
- MEDIUM
|
|
40
|
+
- LOW
|
|
41
|
+
max_vulnerabilities:
|
|
42
|
+
description: 'Maximum vulnerabilities to fix (0 = unlimited)'
|
|
43
|
+
required: false
|
|
44
|
+
default: '5'
|
|
45
|
+
type: string
|
|
46
|
+
llm_model:
|
|
47
|
+
description: 'LLM model to use for remediation'
|
|
48
|
+
required: false
|
|
49
|
+
default: 'anthropic/claude-sonnet-4-5-20250929'
|
|
50
|
+
type: string
|
|
51
|
+
llm_base_url:
|
|
52
|
+
description: 'Custom LLM base URL (optional)'
|
|
53
|
+
required: false
|
|
54
|
+
default: ''
|
|
55
|
+
type: string
|
|
56
|
+
|
|
57
|
+
permissions:
|
|
58
|
+
contents: write
|
|
59
|
+
pull-requests: write
|
|
60
|
+
security-events: read
|
|
61
|
+
|
|
62
|
+
jobs:
|
|
63
|
+
scan-and-remediate:
|
|
64
|
+
runs-on: ubuntu-latest
|
|
65
|
+
timeout-minutes: 60
|
|
66
|
+
|
|
67
|
+
steps:
|
|
68
|
+
# Uses @main to auto-update when the action is improved
|
|
69
|
+
- name: Run Vulnerability Remediation
|
|
70
|
+
id: remediate
|
|
71
|
+
uses: OpenHands/extensions/plugins/vulnerability-remediation@main
|
|
72
|
+
with:
|
|
73
|
+
severity-threshold: ${{ inputs.severity_threshold || 'HIGH' }}
|
|
74
|
+
max-vulnerabilities: ${{ inputs.max_vulnerabilities || '5' }}
|
|
75
|
+
llm-model: ${{ inputs.llm_model || 'anthropic/claude-sonnet-4-5-20250929' }}
|
|
76
|
+
llm-base-url: ${{ inputs.llm_base_url || '' }}
|
|
77
|
+
llm-api-key: ${{ secrets.LLM_API_KEY }}
|
|
78
|
+
github-token: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC || secrets.GITHUB_TOKEN }}
|
|
79
|
+
|
|
80
|
+
- name: Summary
|
|
81
|
+
run: |
|
|
82
|
+
echo "### Vulnerability Scan Results" >> $GITHUB_STEP_SUMMARY
|
|
83
|
+
if [ "${{ steps.remediate.outputs.scan-only }}" == "true" ]; then
|
|
84
|
+
echo "✅ No vulnerabilities found that need remediation." >> $GITHUB_STEP_SUMMARY
|
|
85
|
+
else
|
|
86
|
+
echo "🔍 Found ${{ steps.remediate.outputs.vulnerabilities-found }} vulnerabilities to remediate." >> $GITHUB_STEP_SUMMARY
|
|
87
|
+
fi
|
package/pyproject.toml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
|
3
|
+
"bump-minor-pre-major": true,
|
|
4
|
+
"bootstrap-sha": "5dd68ff47a197a7f8e7cdd7a32a6dcb3702cbd7f",
|
|
5
|
+
"packages": {
|
|
6
|
+
".": {
|
|
7
|
+
"release-type": "node",
|
|
8
|
+
"changelog-type": "github",
|
|
9
|
+
"include-component-in-tag": false,
|
|
10
|
+
"skip-changelog": true,
|
|
11
|
+
"extra-files": [
|
|
12
|
+
{ "type": "toml", "path": "pyproject.toml", "jsonpath": "$.project.version" }
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|