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.
Files changed (60) hide show
  1. cve_agent/AGENT_INSTRUCTIONS.md +161 -0
  2. cve_agent/__init__.py +138 -0
  3. cve_agent/__main__.py +341 -0
  4. cve_agent/agents/yocto-cve-backport-interactive.json +52 -0
  5. cve_agent/agents/yocto-cve-backport.json +47 -0
  6. cve_agent/backend.py +173 -0
  7. cve_agent/context.py +435 -0
  8. cve_agent/corrector.py +123 -0
  9. cve_agent/git.py +262 -0
  10. cve_agent/knowledge.py +311 -0
  11. cve_agent/orchestrator.py +425 -0
  12. cve_agent/py.typed +0 -0
  13. cve_agent/review.py +312 -0
  14. cve_agent/session.py +359 -0
  15. cve_agent/setup.py +147 -0
  16. cve_corrector/__init__.py +39 -0
  17. cve_corrector/__main__.py +279 -0
  18. cve_corrector/bitbake_ops.py +214 -0
  19. cve_corrector/blame.py +334 -0
  20. cve_corrector/cherry_pick.py +299 -0
  21. cve_corrector/git_ops.py +296 -0
  22. cve_corrector/meta_layer.py +255 -0
  23. cve_corrector/patch_ops.py +136 -0
  24. cve_corrector/ptest.py +131 -0
  25. cve_corrector/py.typed +0 -0
  26. cve_corrector/recipe_ops.py +344 -0
  27. cve_corrector/state.py +200 -0
  28. cve_corrector/ui.py +87 -0
  29. cve_corrector/utils.py +93 -0
  30. cve_corrector/version.py +75 -0
  31. cve_corrector/workflow.py +591 -0
  32. cve_corrector/workspace.py +233 -0
  33. cve_metadata_extractor/__init__.py +19 -0
  34. cve_metadata_extractor/__main__.py +421 -0
  35. cve_metadata_extractor/config.json +13 -0
  36. cve_metadata_extractor/config.py +56 -0
  37. cve_metadata_extractor/cve_sources.py +57 -0
  38. cve_metadata_extractor/cvelistv5.py +188 -0
  39. cve_metadata_extractor/debian.py +552 -0
  40. cve_metadata_extractor/mirrors.py +143 -0
  41. cve_metadata_extractor/oe_status.py +274 -0
  42. cve_metadata_extractor/osv.py +190 -0
  43. cve_metadata_extractor/processing.py +114 -0
  44. cve_metadata_extractor/py.typed +0 -0
  45. cve_metadata_extractor/sources.py +97 -0
  46. cve_metadata_extractor/ubuntu.py +189 -0
  47. cve_metadata_extractor/utils.py +123 -0
  48. shared/__init__.py +38 -0
  49. shared/exit_codes.py +26 -0
  50. shared/git_runner.py +68 -0
  51. shared/json_cache.py +48 -0
  52. shared/paths.py +27 -0
  53. shared/py.typed +0 -0
  54. shared/url_parser.py +284 -0
  55. yocto_security_tools-1.0.0.dist-info/METADATA +27 -0
  56. yocto_security_tools-1.0.0.dist-info/RECORD +60 -0
  57. yocto_security_tools-1.0.0.dist-info/WHEEL +5 -0
  58. yocto_security_tools-1.0.0.dist-info/entry_points.txt +4 -0
  59. yocto_security_tools-1.0.0.dist-info/licenses/LICENSE +21 -0
  60. 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
+ }