yocto-security-tools 1.0.0__py3-none-any.whl
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.
- cve_agent/AGENT_INSTRUCTIONS.md +161 -0
- cve_agent/__init__.py +138 -0
- cve_agent/__main__.py +341 -0
- cve_agent/agents/yocto-cve-backport-interactive.json +52 -0
- cve_agent/agents/yocto-cve-backport.json +47 -0
- cve_agent/backend.py +173 -0
- cve_agent/context.py +435 -0
- cve_agent/corrector.py +123 -0
- cve_agent/git.py +262 -0
- cve_agent/knowledge.py +311 -0
- cve_agent/orchestrator.py +425 -0
- cve_agent/py.typed +0 -0
- cve_agent/review.py +312 -0
- cve_agent/session.py +359 -0
- cve_agent/setup.py +147 -0
- cve_corrector/__init__.py +39 -0
- cve_corrector/__main__.py +279 -0
- cve_corrector/bitbake_ops.py +214 -0
- cve_corrector/blame.py +334 -0
- cve_corrector/cherry_pick.py +299 -0
- cve_corrector/git_ops.py +296 -0
- cve_corrector/meta_layer.py +255 -0
- cve_corrector/patch_ops.py +136 -0
- cve_corrector/ptest.py +131 -0
- cve_corrector/py.typed +0 -0
- cve_corrector/recipe_ops.py +344 -0
- cve_corrector/state.py +200 -0
- cve_corrector/ui.py +87 -0
- cve_corrector/utils.py +93 -0
- cve_corrector/version.py +75 -0
- cve_corrector/workflow.py +591 -0
- cve_corrector/workspace.py +233 -0
- cve_metadata_extractor/__init__.py +19 -0
- cve_metadata_extractor/__main__.py +421 -0
- cve_metadata_extractor/config.json +13 -0
- cve_metadata_extractor/config.py +56 -0
- cve_metadata_extractor/cve_sources.py +57 -0
- cve_metadata_extractor/cvelistv5.py +188 -0
- cve_metadata_extractor/debian.py +552 -0
- cve_metadata_extractor/mirrors.py +143 -0
- cve_metadata_extractor/oe_status.py +274 -0
- cve_metadata_extractor/osv.py +190 -0
- cve_metadata_extractor/processing.py +114 -0
- cve_metadata_extractor/py.typed +0 -0
- cve_metadata_extractor/sources.py +97 -0
- cve_metadata_extractor/ubuntu.py +189 -0
- cve_metadata_extractor/utils.py +123 -0
- shared/__init__.py +38 -0
- shared/exit_codes.py +26 -0
- shared/git_runner.py +68 -0
- shared/json_cache.py +48 -0
- shared/paths.py +27 -0
- shared/py.typed +0 -0
- shared/url_parser.py +284 -0
- yocto_security_tools-1.0.0.dist-info/METADATA +27 -0
- yocto_security_tools-1.0.0.dist-info/RECORD +60 -0
- yocto_security_tools-1.0.0.dist-info/WHEEL +5 -0
- yocto_security_tools-1.0.0.dist-info/entry_points.txt +4 -0
- yocto_security_tools-1.0.0.dist-info/licenses/LICENSE +21 -0
- yocto_security_tools-1.0.0.dist-info/top_level.txt +4 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: MIT -->
|
|
2
|
+
# CVE Backport Agent Instructions
|
|
3
|
+
|
|
4
|
+
## Scope Rules
|
|
5
|
+
|
|
6
|
+
You may ONLY modify files listed in the **Allowed Files** section of the context header.
|
|
7
|
+
A git pre-commit hook enforces this — commits with unauthorized files will be rejected.
|
|
8
|
+
|
|
9
|
+
**NEVER do any of these:**
|
|
10
|
+
- `git add .` or `git add -A`
|
|
11
|
+
- `git commit --no-verify` or `git cherry-pick --no-verify`
|
|
12
|
+
- Cherry-pick additional upstream commits beyond what cve_corrector already applied
|
|
13
|
+
- Create or rename files not in the Allowed Files list
|
|
14
|
+
- Modify `.gitignore` or any file not in the Allowed Files list
|
|
15
|
+
- Run `cve_corrector.py` (the agent handles workflow progression)
|
|
16
|
+
- Read files outside the workspace directory
|
|
17
|
+
- Use the `glob` tool
|
|
18
|
+
|
|
19
|
+
**Prerequisite commits**: If the upstream fix depends on a prior commit, do NOT
|
|
20
|
+
cherry-pick it separately. Instead, manually adapt the conflicting code to work
|
|
21
|
+
without the prerequisite — inline the necessary changes into the files already
|
|
22
|
+
being modified. The generated patch must only contain the upstream fix commit's
|
|
23
|
+
changes, adapted for the stable branch.
|
|
24
|
+
|
|
25
|
+
**Files not in the baseline**: If the upstream commit adds a NEW file that is
|
|
26
|
+
in the Allowed Files list, include it — `git cherry-pick` will stage it
|
|
27
|
+
automatically. If it conflicts or requires infrastructure not present in the
|
|
28
|
+
stable branch, mention it in the commit message as:
|
|
29
|
+
`<file>: omitted (depends on <missing infrastructure>)`
|
|
30
|
+
|
|
31
|
+
Only omit a file if including it would break the build or if it depends on
|
|
32
|
+
code/headers/build rules that don't exist in the stable branch.
|
|
33
|
+
|
|
34
|
+
## Workflow
|
|
35
|
+
|
|
36
|
+
### 1. Analyse (always)
|
|
37
|
+
```bash
|
|
38
|
+
git log original-version..HEAD --oneline # what was applied
|
|
39
|
+
git show HEAD # understand the fix
|
|
40
|
+
```
|
|
41
|
+
If the patch is incompatible with the stable base, adapt it.
|
|
42
|
+
|
|
43
|
+
If the CVE fix is **not applicable** to this version (e.g. the vulnerable code
|
|
44
|
+
path, function, struct, or feature does not exist in the stable branch), do NOT
|
|
45
|
+
make any code changes. Instead, write a conclusion file:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cat > "<agent_dir>/conclusion.json" <<'EOF'
|
|
49
|
+
{"not_applicable": true, "reason": "<one-line explanation of why the CVE does not apply>"}
|
|
50
|
+
EOF
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Replace `<agent_dir>` with the actual agent dir path from the context header.
|
|
54
|
+
The reason should be specific — mention the missing function, struct, code path,
|
|
55
|
+
or feature and the version. Example:
|
|
56
|
+
`"PBMAC1 infrastructure (PBMAC1PARAM, PBMAC1_get1_pbkdf2_param) does not exist in 3.2.6; CVE-2025-11187 is not applicable to this version"`
|
|
57
|
+
|
|
58
|
+
After writing the conclusion file, **stop — do not make any other changes.**
|
|
59
|
+
|
|
60
|
+
### 2. Resolve Conflicts (exit code 1)
|
|
61
|
+
```bash
|
|
62
|
+
git status && git diff # examine conflicts
|
|
63
|
+
git show <upstream_sha> # upstream fix intent
|
|
64
|
+
git log --oneline -20 -- <file> # file history for context
|
|
65
|
+
```
|
|
66
|
+
Resolve conflicts, then:
|
|
67
|
+
```bash
|
|
68
|
+
git add <resolved_files> # ONLY allowed files
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
If you adapted the patch (not a verbatim cherry-pick), append your backport
|
|
72
|
+
notes to `.git/MERGE_MSG` — **read the file first**, keep the original content,
|
|
73
|
+
and append your notes after a blank line. Then:
|
|
74
|
+
```bash
|
|
75
|
+
git cherry-pick --no-edit --continue
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 3. Fix Build Errors (exit code 4)
|
|
79
|
+
Read the last 50 lines of the build log. If the failing task belongs to a
|
|
80
|
+
**different recipe** than the one being patched, **abort immediately** — do not
|
|
81
|
+
attempt to fix it. This indicates a pre-existing or environmental issue.
|
|
82
|
+
Otherwise, fix the code and amend the commit.
|
|
83
|
+
|
|
84
|
+
### 4. Fix Test Failures (exit code 3)
|
|
85
|
+
Fix the **backported code in the allowed files only**.
|
|
86
|
+
If the fix requires changing a file not in the allowed list, stop and
|
|
87
|
+
flag for human review. Document which tests failed and what code change
|
|
88
|
+
fixed them in the commit message.
|
|
89
|
+
|
|
90
|
+
### 5. Build Verification (mandatory after every change)
|
|
91
|
+
```bash
|
|
92
|
+
BUILD_LOG="<agent_dir>/build_$$.log"
|
|
93
|
+
devtool build <recipe> > "$BUILD_LOG" 2>&1
|
|
94
|
+
echo "Exit code: $?"
|
|
95
|
+
```
|
|
96
|
+
On failure: `tail -50 "$BUILD_LOG"`, fix, `git commit --amend --no-edit`, retry.
|
|
97
|
+
If `devtool build` logs are insufficient, check Yocto task logs at:
|
|
98
|
+
`<yocto_tmp>/work/<arch>/<recipe>/*/temp/log.do_compile`
|
|
99
|
+
(paths are in the context header).
|
|
100
|
+
On success: **stop — your work is done.**
|
|
101
|
+
|
|
102
|
+
For cross-compilation: use `bitbake -c devshell <recipe>`, never run
|
|
103
|
+
make/cmake/gcc directly.
|
|
104
|
+
|
|
105
|
+
## Resolution Principles
|
|
106
|
+
|
|
107
|
+
- **Minimal changes only** — smallest adaptation to make the fix work on stable
|
|
108
|
+
- **Preserve upstream intent** — adapt APIs/signatures, never change fix logic
|
|
109
|
+
- **Match surrounding whitespace** — use the same indentation style (tabs vs spaces, alignment width) as the surrounding code in the stable branch, not the upstream patch
|
|
110
|
+
- **Check dependencies** — look for `Link:` in commit, prerequisite patches
|
|
111
|
+
- **If uncertain, stop** — flag for human review rather than guess
|
|
112
|
+
|
|
113
|
+
## Common Conflict Patterns
|
|
114
|
+
|
|
115
|
+
| Pattern | Resolution | Commit Note |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| Function signature changed | Keep fix logic, adapt to stable signature | `Adapted foo_v2() to foo_v1() API` |
|
|
118
|
+
| Struct member renamed | Use stable member name with upstream logic | `Member renamed netdev→ndev in original patch` |
|
|
119
|
+
| Function moved to different file | Apply fix where function lives in stable | `Function in old_file.c in original patch` |
|
|
120
|
+
| Missing helper function | Inline it or use stable equivalent | `Inlined helper_foo() (not in stable)` |
|
|
121
|
+
|
|
122
|
+
## Commit Message Format
|
|
123
|
+
|
|
124
|
+
**IMPORTANT: Preserve the original upstream commit message.** The `.git/MERGE_MSG`
|
|
125
|
+
file contains the original upstream commit subject and body. You MUST keep it
|
|
126
|
+
intact and only **append** your backport notes after it. Never replace or rewrite
|
|
127
|
+
the original message.
|
|
128
|
+
|
|
129
|
+
Only append notes if you adapted the patch. Use EXACTLY this markdown format — no
|
|
130
|
+
alternative headers like "Conflict resolution notes:" or "Backport changes:".
|
|
131
|
+
|
|
132
|
+
Append the following block after the original commit message (separated by a
|
|
133
|
+
blank line):
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
Backport Resolution: <One or two sentences explaining what the upstream commit does — the functional
|
|
137
|
+
change, not the conflict details.>
|
|
138
|
+
|
|
139
|
+
Conflicts Resolved:
|
|
140
|
+
|
|
141
|
+
<file> (<N> conflict[s]):
|
|
142
|
+
- <What was changed and why, referencing stable vs upstream differences.>
|
|
143
|
+
|
|
144
|
+
<file> (<N> conflict[s]):
|
|
145
|
+
- <What was changed and why.>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Rules:
|
|
149
|
+
- **Never delete or rewrite the original subject line or body** from `.git/MERGE_MSG`
|
|
150
|
+
- Append your notes after the existing message, separated by a blank line
|
|
151
|
+
- Start with a summary of the upstream fix's purpose (what it changes, why)
|
|
152
|
+
- List ONLY files that had conflicts or required adaptation — skip clean files
|
|
153
|
+
- For each file, state the conflict count and describe each adaptation
|
|
154
|
+
- Mention specific function names, types, APIs, and why the stable branch differs
|
|
155
|
+
- Omitted files: `<file>: omitted (not in branch)`
|
|
156
|
+
- Do NOT add a "Changes from upstream" section (the agent generates that)
|
|
157
|
+
- If you adapted the patch (not a verbatim cherry-pick), add a trailer line
|
|
158
|
+
after a blank line at the end of the commit message:
|
|
159
|
+
`Assisted-by: <backend>:<model>` where `<backend>` and `<model>` are the
|
|
160
|
+
**Backend** and **Model** values from the context header (e.g.
|
|
161
|
+
`Assisted-by: kiro:claude-sonnet-4-20250514`)
|
cve_agent/__init__.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Copyright (C) 2026 Ericsson AB
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""CVE Agent - Orchestrates CVE backporting with AI-assisted conflict resolution.
|
|
4
|
+
|
|
5
|
+
Wraps cve_corrector and spawns AI sessions to resolve conflicts, build errors,
|
|
6
|
+
and test failures during CVE backporting.
|
|
7
|
+
"""
|
|
8
|
+
import sys
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
# Exit codes (single source of truth: shared/exit_codes.py)
|
|
15
|
+
from shared.exit_codes import (
|
|
16
|
+
EXIT_AGENT_ERROR,
|
|
17
|
+
EXIT_AI_TIMEOUT,
|
|
18
|
+
EXIT_ALREADY_APPLIED,
|
|
19
|
+
EXIT_BUILD_ERROR,
|
|
20
|
+
EXIT_BUILD_PREEXISTING,
|
|
21
|
+
EXIT_CHECKOUT_ERROR,
|
|
22
|
+
EXIT_CONFLICT,
|
|
23
|
+
EXIT_DEVTOOL_ERROR,
|
|
24
|
+
EXIT_GIT_ERROR,
|
|
25
|
+
EXIT_METADATA_ERROR,
|
|
26
|
+
EXIT_NOT_APPLICABLE,
|
|
27
|
+
EXIT_PATCH_ERROR,
|
|
28
|
+
EXIT_PTEST_ERROR,
|
|
29
|
+
EXIT_PTEST_PREEXISTING,
|
|
30
|
+
EXIT_SUCCESS,
|
|
31
|
+
EXIT_TRUST_DECLINED,
|
|
32
|
+
)
|
|
33
|
+
from shared.paths import data_dir
|
|
34
|
+
|
|
35
|
+
# Exit codes that trigger the resolution loop (agent can attempt to fix)
|
|
36
|
+
RECOVERABLE_EXITS = {EXIT_CONFLICT, EXIT_PTEST_ERROR, EXIT_BUILD_ERROR}
|
|
37
|
+
|
|
38
|
+
# Exit codes that require immediate escalation (no point retrying)
|
|
39
|
+
UNRECOVERABLE_EXITS = {EXIT_CHECKOUT_ERROR, EXIT_PATCH_ERROR,
|
|
40
|
+
EXIT_METADATA_ERROR, EXIT_GIT_ERROR,
|
|
41
|
+
EXIT_PTEST_PREEXISTING, EXIT_DEVTOOL_ERROR,
|
|
42
|
+
EXIT_BUILD_PREEXISTING, EXIT_NOT_APPLICABLE}
|
|
43
|
+
|
|
44
|
+
# Default paths
|
|
45
|
+
DEFAULT_KNOWLEDGE_PATH = data_dir() / 'knowledge.json'
|
|
46
|
+
DEFAULT_MAX_RETRIES = 3
|
|
47
|
+
DEFAULT_SESSION_TIMEOUT = 600
|
|
48
|
+
CORRECTOR_CMD = [sys.executable, '-m', 'cve_corrector']
|
|
49
|
+
AGENT_INSTRUCTIONS = Path(__file__).resolve().parent / 'AGENT_INSTRUCTIONS.md'
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ResultStatus(Enum):
|
|
53
|
+
"""Outcome status for a CVE processing attempt."""
|
|
54
|
+
SUCCESS = "success"
|
|
55
|
+
CONFLICT_RESOLVED = "conflict_resolved"
|
|
56
|
+
FAILED = "failed"
|
|
57
|
+
ESCALATED = "escalated"
|
|
58
|
+
SKIPPED = "skipped"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class AgentConfig:
|
|
63
|
+
"""Configuration for a single CVE agent run."""
|
|
64
|
+
cve_id: str
|
|
65
|
+
cve_info_path: Optional[Path] = None
|
|
66
|
+
trust_mode: bool = False
|
|
67
|
+
max_retries: int = DEFAULT_MAX_RETRIES
|
|
68
|
+
max_total_attempts: int = 0 # 0 = no cap beyond per-step max_retries
|
|
69
|
+
mirror_dir: Optional[Path] = None
|
|
70
|
+
meta_layer: Optional[Path] = None
|
|
71
|
+
skip_ptest: bool = False
|
|
72
|
+
clean: bool = False
|
|
73
|
+
model: str = "claude-sonnet-4.6"
|
|
74
|
+
session_timeout: int = DEFAULT_SESSION_TIMEOUT
|
|
75
|
+
interactive: bool = False
|
|
76
|
+
bbappend: bool = False
|
|
77
|
+
skip_cve_applicability: bool = False
|
|
78
|
+
fix_url: Optional[str] = None
|
|
79
|
+
recipe: Optional[str] = None
|
|
80
|
+
backend: str = "kiro"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class CveResult:
|
|
85
|
+
"""Outcome of processing a single CVE."""
|
|
86
|
+
cve_id: str
|
|
87
|
+
status: ResultStatus
|
|
88
|
+
retries: int = 0
|
|
89
|
+
duration: float = 0.0
|
|
90
|
+
resolution_summary: str = ""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_build_dir(workspace_path: Path) -> Path:
|
|
94
|
+
"""Derive the Yocto build directory from a devtool workspace path.
|
|
95
|
+
|
|
96
|
+
The devtool workspace structure is: <build>/workspace/sources/<recipe>,
|
|
97
|
+
so the build directory is three levels up from the workspace path.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
workspace_path: Path to the devtool workspace source directory.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Path to the Yocto build directory.
|
|
104
|
+
"""
|
|
105
|
+
return workspace_path.parent.parent.parent
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_agent_dir(workspace_path: Path) -> Path:
|
|
109
|
+
"""Get or create the agent working directory outside the git workspace.
|
|
110
|
+
|
|
111
|
+
Uses the build workspace's cve_agent/ directory to avoid polluting
|
|
112
|
+
the source git repo with agent artifacts.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
workspace_path: Path to the devtool workspace source directory.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Path to the cve_agent/<recipe> directory, created if needed.
|
|
119
|
+
"""
|
|
120
|
+
recipe = workspace_path.name
|
|
121
|
+
build_workspace = workspace_path.parent.parent
|
|
122
|
+
agent_dir = build_workspace / 'cve_agent' / recipe
|
|
123
|
+
agent_dir.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
return agent_dir
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
__all__ = [
|
|
128
|
+
'AgentConfig', 'CveResult', 'ResultStatus',
|
|
129
|
+
'RECOVERABLE_EXITS', 'UNRECOVERABLE_EXITS',
|
|
130
|
+
'DEFAULT_KNOWLEDGE_PATH', 'DEFAULT_MAX_RETRIES', 'DEFAULT_SESSION_TIMEOUT',
|
|
131
|
+
'CORRECTOR_CMD', 'AGENT_INSTRUCTIONS',
|
|
132
|
+
'get_build_dir', 'get_agent_dir',
|
|
133
|
+
'EXIT_SUCCESS', 'EXIT_CONFLICT', 'EXIT_CHECKOUT_ERROR', 'EXIT_PTEST_ERROR',
|
|
134
|
+
'EXIT_BUILD_ERROR', 'EXIT_PATCH_ERROR', 'EXIT_METADATA_ERROR', 'EXIT_GIT_ERROR',
|
|
135
|
+
'EXIT_PTEST_PREEXISTING', 'EXIT_DEVTOOL_ERROR', 'EXIT_BUILD_PREEXISTING',
|
|
136
|
+
'EXIT_ALREADY_APPLIED', 'EXIT_NOT_APPLICABLE',
|
|
137
|
+
'EXIT_TRUST_DECLINED', 'EXIT_AGENT_ERROR', 'EXIT_AI_TIMEOUT',
|
|
138
|
+
]
|
cve_agent/__main__.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Copyright (C) 2026 Ericsson AB
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
"""CVE Backporting Agent — CLI entry point and batch processing.
|
|
5
|
+
|
|
6
|
+
Run with: python3 -m cve_agent [options]
|
|
7
|
+
"""
|
|
8
|
+
import argparse
|
|
9
|
+
import dataclasses
|
|
10
|
+
import os
|
|
11
|
+
import signal
|
|
12
|
+
import sys
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from shared.paths import data_dir
|
|
18
|
+
|
|
19
|
+
from . import (
|
|
20
|
+
DEFAULT_MAX_RETRIES,
|
|
21
|
+
DEFAULT_SESSION_TIMEOUT,
|
|
22
|
+
EXIT_AGENT_ERROR,
|
|
23
|
+
EXIT_TRUST_DECLINED,
|
|
24
|
+
AgentConfig,
|
|
25
|
+
CveResult,
|
|
26
|
+
ResultStatus,
|
|
27
|
+
)
|
|
28
|
+
from .corrector import get_workspace_path, load_cve_metadata
|
|
29
|
+
from .git import run_git_stdout
|
|
30
|
+
from .knowledge import KnowledgeBase
|
|
31
|
+
from .orchestrator import process_single_cve
|
|
32
|
+
from .setup import ensure_agents
|
|
33
|
+
|
|
34
|
+
logger = __import__('logging').getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _get_version() -> str:
|
|
38
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
39
|
+
try:
|
|
40
|
+
return version('yocto-security-tools')
|
|
41
|
+
except PackageNotFoundError:
|
|
42
|
+
return 'dev'
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# --- Trust Mode Warning ---
|
|
46
|
+
|
|
47
|
+
def _show_trust_warning() -> bool:
|
|
48
|
+
"""Display trust mode warning and require explicit confirmation."""
|
|
49
|
+
print(
|
|
50
|
+
"\n\u26a0\ufe0f WARNING: --trust mode enabled. The agent will operate "
|
|
51
|
+
"without human review.\n\n"
|
|
52
|
+
"This is NOT recommended. Automated conflict resolution may:\n"
|
|
53
|
+
" - Introduce subtle bugs that change fix semantics\n"
|
|
54
|
+
" - Miss context that requires human judgment\n"
|
|
55
|
+
" - Produce patches that pass build/ptest but are logically "
|
|
56
|
+
"incorrect\n\n"
|
|
57
|
+
"Human review of conflict resolutions is strongly recommended.\n"
|
|
58
|
+
)
|
|
59
|
+
response = input("Continue in trust mode? [y/N]: ").strip().lower()
|
|
60
|
+
return response == 'y'
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# --- Logging ---
|
|
64
|
+
|
|
65
|
+
def _log_result(config: AgentConfig, result: CveResult,
|
|
66
|
+
workspace_path: Optional[Path] = None) -> None:
|
|
67
|
+
"""Append result entry to the CVE agent log file."""
|
|
68
|
+
bbpath = os.environ.get('BBPATH', '')
|
|
69
|
+
if not bbpath:
|
|
70
|
+
return
|
|
71
|
+
build_ws = Path(bbpath.split(':')[0]) / 'workspace' / 'cve_agent'
|
|
72
|
+
build_ws.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
log_file = build_ws / 'cve_agent.log'
|
|
74
|
+
|
|
75
|
+
lines = [
|
|
76
|
+
f"[{datetime.now(timezone.utc).isoformat()}] "
|
|
77
|
+
f"{result.cve_id} | {result.status.value} | "
|
|
78
|
+
f"{result.duration:.1f}s | retries={result.retries} | "
|
|
79
|
+
f"{result.resolution_summary}"
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
ws_path = workspace_path
|
|
83
|
+
if ws_path is None:
|
|
84
|
+
try:
|
|
85
|
+
cve_data = load_cve_metadata(config.cve_info_path)
|
|
86
|
+
ws_path = get_workspace_path(config, cve_data)
|
|
87
|
+
except Exception:
|
|
88
|
+
logger.debug("Could not resolve workspace for %s", result.cve_id, exc_info=True)
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
if ws_path:
|
|
92
|
+
diff_stat = run_git_stdout(['diff', '--stat', 'original-version..HEAD'], ws_path)
|
|
93
|
+
if diff_stat:
|
|
94
|
+
lines.append(f" diff-stat: {diff_stat}")
|
|
95
|
+
diff = run_git_stdout(['diff', 'original-version..HEAD'], ws_path)
|
|
96
|
+
if diff:
|
|
97
|
+
if len(diff) > 50_000:
|
|
98
|
+
diff = diff[:50_000] + "\n... (truncated, >50KB)"
|
|
99
|
+
lines.append(f" diff:\n{diff}")
|
|
100
|
+
except Exception:
|
|
101
|
+
logger.debug("Failed to capture diff for %s", result.cve_id, exc_info=True)
|
|
102
|
+
|
|
103
|
+
with open(log_file, 'a', encoding='utf-8') as log_fh:
|
|
104
|
+
log_fh.write('\n'.join(lines) + '\n\n')
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# --- Batch Processing ---
|
|
108
|
+
|
|
109
|
+
def _process_batch(cve_list: list[str], config_template: AgentConfig,
|
|
110
|
+
knowledge_base: KnowledgeBase) -> list[CveResult]:
|
|
111
|
+
"""Process a list of CVEs sequentially."""
|
|
112
|
+
results: list[CveResult] = []
|
|
113
|
+
total = len(cve_list)
|
|
114
|
+
|
|
115
|
+
for idx, cve_id in enumerate(cve_list, 1):
|
|
116
|
+
print(f"\n[{idx}/{total}] {cve_id}")
|
|
117
|
+
config = dataclasses.replace(config_template, cve_id=cve_id)
|
|
118
|
+
|
|
119
|
+
result = process_single_cve(config, knowledge_base)
|
|
120
|
+
_log_result(config, result)
|
|
121
|
+
results.append(result)
|
|
122
|
+
print(f" Result: {result.status.value} — {result.resolution_summary}")
|
|
123
|
+
|
|
124
|
+
if result.status in (ResultStatus.FAILED, ResultStatus.ESCALATED) and not config_template.trust_mode:
|
|
125
|
+
response = input(
|
|
126
|
+
"Skip and continue to next CVE? [Y/n]: "
|
|
127
|
+
).strip().lower()
|
|
128
|
+
if response in ('n', 'no'):
|
|
129
|
+
break
|
|
130
|
+
|
|
131
|
+
return results
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _print_batch_summary(results: list[CveResult]) -> None:
|
|
135
|
+
"""Print a summary of batch processing results."""
|
|
136
|
+
print(f"\n{'=' * 60}")
|
|
137
|
+
print("BATCH SUMMARY")
|
|
138
|
+
print(f"{'=' * 60}")
|
|
139
|
+
print(f"Total CVEs processed: {len(results)}")
|
|
140
|
+
|
|
141
|
+
counts: dict[str, int] = {}
|
|
142
|
+
for result in results:
|
|
143
|
+
counts[result.status.value] = counts.get(result.status.value, 0) + 1
|
|
144
|
+
|
|
145
|
+
for status, count in sorted(counts.items()):
|
|
146
|
+
print(f" {status}: {count}")
|
|
147
|
+
|
|
148
|
+
print("\nPer-CVE results:")
|
|
149
|
+
for result in results:
|
|
150
|
+
retries_info = f" ({result.retries} retries)" if result.retries else ""
|
|
151
|
+
print(f" {result.cve_id}: {result.status.value}{retries_info}")
|
|
152
|
+
|
|
153
|
+
print(f"{'=' * 60}")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _save_results(results: list[CveResult]) -> None:
|
|
157
|
+
"""Save detailed results to a timestamped file."""
|
|
158
|
+
results_dir = data_dir() / 'results'
|
|
159
|
+
results_dir.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
timestamp = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
|
|
161
|
+
filepath = results_dir / f"backport_agent_results_{timestamp}.txt"
|
|
162
|
+
with open(filepath, 'w', encoding='utf-8') as file:
|
|
163
|
+
file.write(f"CVE Agent Results - {timestamp}\n")
|
|
164
|
+
file.write("=" * 60 + "\n\n")
|
|
165
|
+
for result in results:
|
|
166
|
+
file.write(
|
|
167
|
+
f"{result.cve_id}: {result.status.value} "
|
|
168
|
+
f"(retries={result.retries}, "
|
|
169
|
+
f"duration={result.duration:.1f}s)\n"
|
|
170
|
+
f" {result.resolution_summary}\n\n"
|
|
171
|
+
)
|
|
172
|
+
print(f"Results saved to: {filepath}")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# --- Signal Handling ---
|
|
176
|
+
|
|
177
|
+
def _sigint_handler(results: list[CveResult]):
|
|
178
|
+
"""Return a SIGINT handler that saves partial results on interrupt."""
|
|
179
|
+
def handler(signum, frame) -> None:
|
|
180
|
+
print("\n\nInterrupted by user (Ctrl+C).")
|
|
181
|
+
if results:
|
|
182
|
+
_print_batch_summary(results)
|
|
183
|
+
_save_results(results)
|
|
184
|
+
print(f"\nPartial progress saved ({len(results)} CVEs completed).")
|
|
185
|
+
else:
|
|
186
|
+
print("No results to save.")
|
|
187
|
+
sys.exit(EXIT_AGENT_ERROR)
|
|
188
|
+
return handler
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# --- CLI Entry Point ---
|
|
192
|
+
|
|
193
|
+
def _parse_args() -> argparse.Namespace:
|
|
194
|
+
"""Parse command-line arguments."""
|
|
195
|
+
parser = argparse.ArgumentParser(
|
|
196
|
+
description="CVE Backporting Agent - AI-assisted CVE fix orchestration"
|
|
197
|
+
)
|
|
198
|
+
parser.add_argument('--version', action='version',
|
|
199
|
+
version=f'%(prog)s {_get_version()}')
|
|
200
|
+
|
|
201
|
+
# --- Input ---
|
|
202
|
+
input_group = parser.add_argument_group('input')
|
|
203
|
+
cve_group = input_group.add_mutually_exclusive_group(required=True)
|
|
204
|
+
cve_group.add_argument('--cve-id', help='Single CVE identifier')
|
|
205
|
+
cve_group.add_argument('--cve-list', type=Path,
|
|
206
|
+
help='File with CVE IDs, one per line')
|
|
207
|
+
input_group.add_argument('--cve-info', type=Path,
|
|
208
|
+
help='JSON file with CVE metadata')
|
|
209
|
+
input_group.add_argument('--fix-url',
|
|
210
|
+
help='URL of fix commit or pull request')
|
|
211
|
+
input_group.add_argument('--recipe',
|
|
212
|
+
help='Recipe name (required with --fix-url without --cve-info)')
|
|
213
|
+
|
|
214
|
+
# --- AI session ---
|
|
215
|
+
ai_group = parser.add_argument_group('AI session')
|
|
216
|
+
ai_group.add_argument('--backend', default='kiro',
|
|
217
|
+
help='AI backend to use (default: %(default)s)')
|
|
218
|
+
ai_group.add_argument('--model', default='claude-sonnet-4.6',
|
|
219
|
+
help='Model for AI sessions (default: %(default)s)')
|
|
220
|
+
ai_group.add_argument('--max-retries', type=int, default=DEFAULT_MAX_RETRIES,
|
|
221
|
+
help='Max resolution attempts (default: %(default)s)')
|
|
222
|
+
ai_group.add_argument('--session-timeout', type=int,
|
|
223
|
+
default=DEFAULT_SESSION_TIMEOUT,
|
|
224
|
+
help='Timeout per session in seconds (default: %(default)s)')
|
|
225
|
+
ai_group.add_argument('--trust', action='store_true',
|
|
226
|
+
help='Skip human review (NOT recommended)')
|
|
227
|
+
ai_group.add_argument('--interactive', action='store_true',
|
|
228
|
+
help='Enable interactive mode')
|
|
229
|
+
|
|
230
|
+
# --- Build control ---
|
|
231
|
+
build_group = parser.add_argument_group('build control')
|
|
232
|
+
build_group.add_argument('--skip-ptest', action='store_true',
|
|
233
|
+
help='Skip ptest execution')
|
|
234
|
+
build_group.add_argument('--skip-cve-applicability', action='store_true',
|
|
235
|
+
help='Skip git-blame based CVE applicability check')
|
|
236
|
+
build_group.add_argument('--clean', action='store_true',
|
|
237
|
+
help='Clean workspace before starting')
|
|
238
|
+
|
|
239
|
+
# --- Output ---
|
|
240
|
+
output_group = parser.add_argument_group('output')
|
|
241
|
+
output_group.add_argument('--meta-layer', type=Path,
|
|
242
|
+
help='Destination meta-layer for devtool finish')
|
|
243
|
+
output_group.add_argument('--bbappend', action='store_true',
|
|
244
|
+
help='Create a bbappend instead of modifying the original recipe')
|
|
245
|
+
|
|
246
|
+
# --- Environment ---
|
|
247
|
+
env_group = parser.add_argument_group('environment')
|
|
248
|
+
env_group.add_argument('--mirror-dir', type=Path,
|
|
249
|
+
help='Directory with bare repository mirrors')
|
|
250
|
+
|
|
251
|
+
return parser.parse_args()
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _read_cve_list(cve_list_path: Path) -> list[str]:
|
|
255
|
+
"""Read CVE IDs from a file, one per line."""
|
|
256
|
+
if not cve_list_path.exists():
|
|
257
|
+
print(f"Error: CVE list file not found: {cve_list_path}",
|
|
258
|
+
file=sys.stderr)
|
|
259
|
+
sys.exit(EXIT_AGENT_ERROR)
|
|
260
|
+
|
|
261
|
+
lines = cve_list_path.read_text(encoding='utf-8').splitlines()
|
|
262
|
+
return [line.strip() for line in lines if line.strip()]
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _config_from_args(args: argparse.Namespace,
|
|
266
|
+
cve_id: Optional[str] = None) -> AgentConfig:
|
|
267
|
+
"""Create an AgentConfig from parsed CLI arguments."""
|
|
268
|
+
return AgentConfig(
|
|
269
|
+
cve_id=cve_id if cve_id is not None else (args.cve_id or ""),
|
|
270
|
+
cve_info_path=args.cve_info,
|
|
271
|
+
trust_mode=args.trust,
|
|
272
|
+
max_retries=args.max_retries,
|
|
273
|
+
mirror_dir=args.mirror_dir,
|
|
274
|
+
meta_layer=args.meta_layer,
|
|
275
|
+
skip_ptest=args.skip_ptest,
|
|
276
|
+
clean=args.clean,
|
|
277
|
+
model=args.model,
|
|
278
|
+
session_timeout=args.session_timeout,
|
|
279
|
+
bbappend=args.bbappend,
|
|
280
|
+
skip_cve_applicability=args.skip_cve_applicability,
|
|
281
|
+
interactive=args.interactive,
|
|
282
|
+
fix_url=args.fix_url,
|
|
283
|
+
recipe=args.recipe,
|
|
284
|
+
backend=args.backend,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def main() -> None:
|
|
289
|
+
"""Main entry point for the CVE agent."""
|
|
290
|
+
results: list[CveResult] = []
|
|
291
|
+
signal.signal(signal.SIGINT, _sigint_handler(results))
|
|
292
|
+
args = _parse_args()
|
|
293
|
+
|
|
294
|
+
if not args.cve_info and not args.fix_url:
|
|
295
|
+
print("Error: --cve-info or --fix-url is required", file=sys.stderr)
|
|
296
|
+
sys.exit(EXIT_AGENT_ERROR)
|
|
297
|
+
if args.fix_url and not args.cve_info and not args.recipe:
|
|
298
|
+
print("Error: --recipe is required when using --fix-url without "
|
|
299
|
+
"--cve-info", file=sys.stderr)
|
|
300
|
+
sys.exit(EXIT_AGENT_ERROR)
|
|
301
|
+
|
|
302
|
+
if args.backend == 'kiro':
|
|
303
|
+
ensure_agents(interactive=not args.trust)
|
|
304
|
+
|
|
305
|
+
if args.trust and not _show_trust_warning():
|
|
306
|
+
print("Trust mode declined. Exiting.")
|
|
307
|
+
sys.exit(EXIT_TRUST_DECLINED)
|
|
308
|
+
|
|
309
|
+
knowledge_base = KnowledgeBase()
|
|
310
|
+
|
|
311
|
+
if args.cve_id:
|
|
312
|
+
from .corrector import validate_cve_id
|
|
313
|
+
if not validate_cve_id(args.cve_id):
|
|
314
|
+
print(f"Invalid CVE ID format: {args.cve_id}", file=sys.stderr)
|
|
315
|
+
sys.exit(EXIT_AGENT_ERROR)
|
|
316
|
+
config = _config_from_args(args, args.cve_id)
|
|
317
|
+
result = process_single_cve(config, knowledge_base)
|
|
318
|
+
print(f"\n\u2713 {result.cve_id}: {result.status.value}")
|
|
319
|
+
_log_result(config, result)
|
|
320
|
+
if result.resolution_summary:
|
|
321
|
+
print(f" {result.resolution_summary}")
|
|
322
|
+
if result.status not in (ResultStatus.SUCCESS,
|
|
323
|
+
ResultStatus.CONFLICT_RESOLVED,
|
|
324
|
+
ResultStatus.SKIPPED):
|
|
325
|
+
sys.exit(EXIT_AGENT_ERROR)
|
|
326
|
+
else:
|
|
327
|
+
cve_list = _read_cve_list(args.cve_list)
|
|
328
|
+
config_template = _config_from_args(args)
|
|
329
|
+
results = _process_batch(cve_list, config_template, knowledge_base)
|
|
330
|
+
_print_batch_summary(results)
|
|
331
|
+
_save_results(results)
|
|
332
|
+
failed = sum(
|
|
333
|
+
1 for r in results
|
|
334
|
+
if r.status in (ResultStatus.FAILED, ResultStatus.ESCALATED)
|
|
335
|
+
)
|
|
336
|
+
if failed:
|
|
337
|
+
sys.exit(EXIT_AGENT_ERROR)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
if __name__ == '__main__':
|
|
341
|
+
main()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "yocto-cve-backport-interactive",
|
|
3
|
+
"description": "Interactive CVE backport agent — same capabilities but requires human approval for writes and commands",
|
|
4
|
+
"prompt": "file://cve_agent/AGENT_INSTRUCTIONS.md",
|
|
5
|
+
"tools": ["fs_read", "fs_write", "execute_bash"],
|
|
6
|
+
"allowedTools": ["fs_read"],
|
|
7
|
+
"toolsSettings": {
|
|
8
|
+
"execute_bash": {
|
|
9
|
+
"allowedCommands": [
|
|
10
|
+
"git status",
|
|
11
|
+
"git status --porcelain",
|
|
12
|
+
"git diff",
|
|
13
|
+
"git diff *",
|
|
14
|
+
"git log *",
|
|
15
|
+
"git show *",
|
|
16
|
+
"git add *",
|
|
17
|
+
"git cherry-pick --continue",
|
|
18
|
+
"git cherry-pick --abort",
|
|
19
|
+
"git am --continue",
|
|
20
|
+
"git am --abort",
|
|
21
|
+
"git checkout -- *",
|
|
22
|
+
"git rev-parse *",
|
|
23
|
+
"git merge-base *",
|
|
24
|
+
"git format-patch *",
|
|
25
|
+
"devtool build *",
|
|
26
|
+
"cat *",
|
|
27
|
+
"head *",
|
|
28
|
+
"tail *",
|
|
29
|
+
"find . *",
|
|
30
|
+
"grep *"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"fs_write": {
|
|
34
|
+
"deniedPaths": [
|
|
35
|
+
"/etc/**",
|
|
36
|
+
"/proc/**",
|
|
37
|
+
"/sys/**",
|
|
38
|
+
"~/.ssh/**",
|
|
39
|
+
"~/.aws/**",
|
|
40
|
+
"~/.kiro/**",
|
|
41
|
+
"~/.gitconfig",
|
|
42
|
+
"~/.netrc",
|
|
43
|
+
"**/cve_agent/**/*.py",
|
|
44
|
+
"**/cve_corrector/**/*.py",
|
|
45
|
+
"**/cve_metadata_extractor/**/*.py",
|
|
46
|
+
"**/shared/**/*.py",
|
|
47
|
+
"**/tests/**"
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"welcomeMessage": "CVE backport session ready. I'll resolve conflicts and verify builds — you approve each tool action."
|
|
52
|
+
}
|