jarvis-ai-assistant 0.3.30__py3-none-any.whl → 0.7.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.
- jarvis/__init__.py +1 -1
- jarvis/jarvis_agent/__init__.py +289 -87
- jarvis/jarvis_agent/agent_manager.py +17 -8
- jarvis/jarvis_agent/edit_file_handler.py +374 -86
- jarvis/jarvis_agent/event_bus.py +1 -1
- jarvis/jarvis_agent/file_context_handler.py +79 -0
- jarvis/jarvis_agent/jarvis.py +601 -43
- jarvis/jarvis_agent/main.py +32 -2
- jarvis/jarvis_agent/rewrite_file_handler.py +141 -0
- jarvis/jarvis_agent/run_loop.py +38 -5
- jarvis/jarvis_agent/share_manager.py +8 -1
- jarvis/jarvis_agent/stdio_redirect.py +295 -0
- jarvis/jarvis_agent/task_analyzer.py +5 -2
- jarvis/jarvis_agent/task_planner.py +496 -0
- jarvis/jarvis_agent/utils.py +5 -1
- jarvis/jarvis_agent/web_bridge.py +189 -0
- jarvis/jarvis_agent/web_output_sink.py +53 -0
- jarvis/jarvis_agent/web_server.py +751 -0
- jarvis/jarvis_c2rust/__init__.py +26 -0
- jarvis/jarvis_c2rust/cli.py +613 -0
- jarvis/jarvis_c2rust/collector.py +258 -0
- jarvis/jarvis_c2rust/library_replacer.py +1122 -0
- jarvis/jarvis_c2rust/llm_module_agent.py +1300 -0
- jarvis/jarvis_c2rust/optimizer.py +960 -0
- jarvis/jarvis_c2rust/scanner.py +1681 -0
- jarvis/jarvis_c2rust/transpiler.py +2325 -0
- jarvis/jarvis_code_agent/build_validation_config.py +133 -0
- jarvis/jarvis_code_agent/code_agent.py +1171 -94
- jarvis/jarvis_code_agent/code_analyzer/__init__.py +62 -0
- jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +102 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +59 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +69 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +38 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +44 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +38 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +50 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +93 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +129 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +54 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +154 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
- jarvis/jarvis_code_agent/code_analyzer/context_manager.py +363 -0
- jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
- jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
- jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
- jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
- jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
- jarvis/jarvis_code_agent/code_analyzer/language_support.py +89 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +31 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +231 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +183 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +219 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +209 -0
- jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +451 -0
- jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +77 -0
- jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +48 -0
- jarvis/jarvis_code_agent/lint.py +270 -8
- jarvis/jarvis_code_agent/utils.py +142 -0
- jarvis/jarvis_code_analysis/code_review.py +483 -569
- jarvis/jarvis_data/config_schema.json +97 -8
- jarvis/jarvis_git_utils/git_commiter.py +38 -26
- jarvis/jarvis_mcp/sse_mcp_client.py +2 -2
- jarvis/jarvis_mcp/stdio_mcp_client.py +1 -1
- jarvis/jarvis_memory_organizer/memory_organizer.py +1 -1
- jarvis/jarvis_multi_agent/__init__.py +239 -25
- jarvis/jarvis_multi_agent/main.py +37 -1
- jarvis/jarvis_platform/base.py +103 -51
- jarvis/jarvis_platform/openai.py +26 -1
- jarvis/jarvis_platform/yuanbao.py +1 -1
- jarvis/jarvis_platform_manager/service.py +2 -2
- jarvis/jarvis_rag/cli.py +4 -4
- jarvis/jarvis_sec/__init__.py +3605 -0
- jarvis/jarvis_sec/checkers/__init__.py +32 -0
- jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
- jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
- jarvis/jarvis_sec/cli.py +116 -0
- jarvis/jarvis_sec/report.py +257 -0
- jarvis/jarvis_sec/status.py +264 -0
- jarvis/jarvis_sec/types.py +20 -0
- jarvis/jarvis_sec/workflow.py +219 -0
- jarvis/jarvis_stats/cli.py +1 -1
- jarvis/jarvis_stats/stats.py +1 -1
- jarvis/jarvis_stats/visualizer.py +1 -1
- jarvis/jarvis_tools/cli/main.py +1 -0
- jarvis/jarvis_tools/execute_script.py +46 -9
- jarvis/jarvis_tools/generate_new_tool.py +3 -1
- jarvis/jarvis_tools/read_code.py +275 -12
- jarvis/jarvis_tools/read_symbols.py +141 -0
- jarvis/jarvis_tools/read_webpage.py +5 -3
- jarvis/jarvis_tools/registry.py +73 -35
- jarvis/jarvis_tools/search_web.py +15 -11
- jarvis/jarvis_tools/sub_agent.py +24 -42
- jarvis/jarvis_tools/sub_code_agent.py +14 -13
- jarvis/jarvis_tools/virtual_tty.py +1 -1
- jarvis/jarvis_utils/config.py +187 -35
- jarvis/jarvis_utils/embedding.py +3 -0
- jarvis/jarvis_utils/git_utils.py +181 -6
- jarvis/jarvis_utils/globals.py +3 -3
- jarvis/jarvis_utils/http.py +1 -1
- jarvis/jarvis_utils/input.py +78 -2
- jarvis/jarvis_utils/methodology.py +25 -19
- jarvis/jarvis_utils/utils.py +644 -359
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/METADATA +85 -1
- jarvis_ai_assistant-0.7.0.dist-info/RECORD +192 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/entry_points.txt +4 -0
- jarvis/jarvis_agent/config.py +0 -92
- jarvis/jarvis_tools/edit_file.py +0 -179
- jarvis/jarvis_tools/rewrite_file.py +0 -191
- jarvis_ai_assistant-0.3.30.dist-info/RECORD +0 -137
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/top_level.txt +0 -0
jarvis/jarvis_sec/cli.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Jarvis 安全演进套件 —— 命令行入口(Typer 版本)
|
|
4
|
+
|
|
5
|
+
用法示例:
|
|
6
|
+
- Agent模式(单Agent,逐条子任务分析)
|
|
7
|
+
python -m jarvis.jarvis_sec.cli agent --path ./target_project
|
|
8
|
+
|
|
9
|
+
可选参数:
|
|
10
|
+
|
|
11
|
+
- --output: 最终Markdown报告输出路径(默认 ./report.md)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
import typer
|
|
21
|
+
from jarvis.jarvis_utils.utils import init_env
|
|
22
|
+
# removed: set_config import(避免全局覆盖模型组配置)
|
|
23
|
+
from jarvis.jarvis_sec.workflow import run_with_agent, direct_scan, format_markdown_report
|
|
24
|
+
|
|
25
|
+
app = typer.Typer(
|
|
26
|
+
add_completion=False,
|
|
27
|
+
no_args_is_help=True,
|
|
28
|
+
help="Jarvis 安全演进套件(单Agent逐条子任务分析)",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.command("agent", help="Agent模式(单Agent逐条子任务分析)")
|
|
35
|
+
def agent(
|
|
36
|
+
path: str = typer.Option(..., "--path", "-p", help="待分析的根目录"),
|
|
37
|
+
|
|
38
|
+
llm_group: Optional[str] = typer.Option(
|
|
39
|
+
None, "--llm-group", "-g", help="使用的模型组(仅对本次运行生效,不修改全局配置)"
|
|
40
|
+
),
|
|
41
|
+
output: Optional[str] = typer.Option(
|
|
42
|
+
"report.md", "--output", "-o", help="最终Markdown报告输出路径(默认 ./report.md)"
|
|
43
|
+
),
|
|
44
|
+
|
|
45
|
+
cluster_limit: int = typer.Option(
|
|
46
|
+
50, "--cluster-limit", "-c", help="聚类每批最多处理的告警数(按文件分批聚类,默认50)"
|
|
47
|
+
),
|
|
48
|
+
) -> None:
|
|
49
|
+
# 初始化环境,确保平台/模型等全局配置就绪(避免 NoneType 平台)
|
|
50
|
+
try:
|
|
51
|
+
init_env("欢迎使用 Jarvis 安全套件!", None)
|
|
52
|
+
except Exception:
|
|
53
|
+
# 环境初始化失败不应阻塞CLI基础功能,继续后续流程
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
# 若指定了模型组:仅对本次运行生效,透传给 Agent;不修改全局配置(无需 set_config)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
text: Optional[str] = None
|
|
60
|
+
try:
|
|
61
|
+
text = run_with_agent(
|
|
62
|
+
path,
|
|
63
|
+
llm_group=llm_group,
|
|
64
|
+
cluster_limit=cluster_limit,
|
|
65
|
+
)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
try:
|
|
68
|
+
typer.secho(f"[jarvis_sec] Agent 分析过程出错,将回退到直扫基线(fast):{e}", fg=typer.colors.YELLOW, err=True)
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
text = None
|
|
72
|
+
|
|
73
|
+
if not text or not str(text).strip():
|
|
74
|
+
try:
|
|
75
|
+
typer.secho("[jarvis_sec] Agent 无输出,回退到直扫基线(fast)。", fg=typer.colors.YELLOW, err=True)
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
result = direct_scan(path)
|
|
79
|
+
text = format_markdown_report(result)
|
|
80
|
+
|
|
81
|
+
if output:
|
|
82
|
+
try:
|
|
83
|
+
md_text = text or ""
|
|
84
|
+
try:
|
|
85
|
+
lines = (text or "").splitlines()
|
|
86
|
+
idx = -1
|
|
87
|
+
for i, ln in enumerate(lines):
|
|
88
|
+
if ln.strip().startswith("# Jarvis 安全问题分析报告"):
|
|
89
|
+
idx = i
|
|
90
|
+
break
|
|
91
|
+
if idx >= 0:
|
|
92
|
+
md_text = "\n".join(lines[idx:])
|
|
93
|
+
except Exception:
|
|
94
|
+
md_text = text or ""
|
|
95
|
+
p = Path(output)
|
|
96
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
p.write_text(md_text, encoding="utf-8")
|
|
98
|
+
try:
|
|
99
|
+
typer.secho(f"[jarvis_sec] Markdown 报告已写入: {p}", fg=typer.colors.GREEN)
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
except Exception as e:
|
|
103
|
+
try:
|
|
104
|
+
typer.secho(f"[jarvis_sec] 写入Markdown报告失败: {e}", fg=typer.colors.RED, err=True)
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
typer.echo(text)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def main() -> int:
|
|
111
|
+
app()
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
sys.exit(main())
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
安全分析套件 —— 报告聚合与评分模块
|
|
4
|
+
|
|
5
|
+
目标:
|
|
6
|
+
- 将启发式检查器输出的结构化问题列表进行聚合与评分,生成统一的 JSON 与 Markdown 报告。
|
|
7
|
+
- 与 workflow.direct_scan / 多Agent Aggregator 保持输出结构一致,便于评测解析与专家审阅。
|
|
8
|
+
|
|
9
|
+
输出结构(JSON示例):
|
|
10
|
+
{
|
|
11
|
+
"summary": {
|
|
12
|
+
"total": 0,
|
|
13
|
+
"by_language": {"c/cpp": 0, "rust": 0},
|
|
14
|
+
"by_category": {
|
|
15
|
+
"buffer_overflow": 0, "unsafe_api": 0, "memory_mgmt": 0, "error_handling": 0,
|
|
16
|
+
"unsafe_usage": 0, "concurrency": 0, "ffi": 0
|
|
17
|
+
},
|
|
18
|
+
"top_risk_files": ["path1", "path2"]
|
|
19
|
+
},
|
|
20
|
+
"issues": [
|
|
21
|
+
{
|
|
22
|
+
"id": "C001",
|
|
23
|
+
"language": "c/cpp",
|
|
24
|
+
"category": "unsafe_api",
|
|
25
|
+
"pattern": "strcpy",
|
|
26
|
+
"file": "src/foo.c",
|
|
27
|
+
"line": 123,
|
|
28
|
+
"evidence": "strcpy(dst, src);",
|
|
29
|
+
"preconditions": "N/A",
|
|
30
|
+
"trigger_path": "函数 foobar 调用 strcpy 时,其输入 src 来自于未经校验的网络数据包",
|
|
31
|
+
"consequences": "可能导致缓冲区溢出",
|
|
32
|
+
"suggestions": "使用 strncpy_s 或其他安全函数替代",
|
|
33
|
+
"confidence": 0.85,
|
|
34
|
+
"severity": "high | medium | low",
|
|
35
|
+
"score": 2.55
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
提供的函数:
|
|
41
|
+
- aggregate_issues(issues: List[Union[Issue, Dict]], scanned_root: Optional[str] = None, scanned_files: Optional[int] = None) -> Dict
|
|
42
|
+
- format_markdown_report(report_json: Dict) -> str
|
|
43
|
+
- build_json_and_markdown(issues: List[Union[Issue, Dict]], scanned_root: Optional[str] = None, scanned_files: Optional[int] = None) -> str
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import hashlib
|
|
49
|
+
from typing import Dict, List, Optional, Union
|
|
50
|
+
|
|
51
|
+
# 依赖 Issue 结构,但本模块不直接导入 dataclass,接受 dict/Issue 两种形态
|
|
52
|
+
try:
|
|
53
|
+
from jarvis.jarvis_sec.types import Issue # 类型提示用,避免循环依赖
|
|
54
|
+
except Exception:
|
|
55
|
+
Issue = dict # type: ignore
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------
|
|
59
|
+
# 内部工具
|
|
60
|
+
# ---------------------------
|
|
61
|
+
|
|
62
|
+
_CATEGORY_ORDER = [
|
|
63
|
+
"unsafe_api",
|
|
64
|
+
"buffer_overflow",
|
|
65
|
+
"memory_mgmt",
|
|
66
|
+
"error_handling",
|
|
67
|
+
"unsafe_usage",
|
|
68
|
+
"concurrency",
|
|
69
|
+
"ffi",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
_SEVERITY_WEIGHT = {
|
|
73
|
+
"high": 3.0,
|
|
74
|
+
"medium": 2.0,
|
|
75
|
+
"low": 1.0,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
def _as_dict(item: Union[Issue, Dict]) -> Dict:
|
|
79
|
+
"""
|
|
80
|
+
将 Issue/dataclass 或 dict 统一为 dict。
|
|
81
|
+
"""
|
|
82
|
+
if isinstance(item, dict):
|
|
83
|
+
return item
|
|
84
|
+
# dataclass: 尝试属性访问
|
|
85
|
+
d: Dict = {}
|
|
86
|
+
for k in (
|
|
87
|
+
"language",
|
|
88
|
+
"category",
|
|
89
|
+
"pattern",
|
|
90
|
+
"file",
|
|
91
|
+
"line",
|
|
92
|
+
"evidence",
|
|
93
|
+
"preconditions",
|
|
94
|
+
"trigger_path",
|
|
95
|
+
"consequences",
|
|
96
|
+
"suggestions",
|
|
97
|
+
"confidence",
|
|
98
|
+
"severity",
|
|
99
|
+
):
|
|
100
|
+
v = getattr(item, k, None)
|
|
101
|
+
if v is not None:
|
|
102
|
+
d[k] = v
|
|
103
|
+
return d
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _normalize_issue(i: Dict) -> Dict:
|
|
107
|
+
"""
|
|
108
|
+
归一化字段并补充缺省值。
|
|
109
|
+
"""
|
|
110
|
+
j = {
|
|
111
|
+
"language": i.get("language", "c/cpp" if str(i.get("file", "")).endswith((".c", ".cpp", ".h", ".hpp")) else "rust"),
|
|
112
|
+
"category": i.get("category", "error_handling"),
|
|
113
|
+
"pattern": i.get("pattern", ""),
|
|
114
|
+
"file": i.get("file", ""),
|
|
115
|
+
"line": int(i.get("line", 0) or 0),
|
|
116
|
+
"evidence": i.get("evidence", ""),
|
|
117
|
+
"preconditions": i.get("preconditions", ""),
|
|
118
|
+
"trigger_path": i.get("trigger_path", ""),
|
|
119
|
+
"consequences": i.get("consequences", ""),
|
|
120
|
+
"suggestions": i.get("suggestions", ""),
|
|
121
|
+
"confidence": float(i.get("confidence", 0.6)),
|
|
122
|
+
"severity": i.get("severity", "medium"),
|
|
123
|
+
}
|
|
124
|
+
# 计算稳定ID(基于文件/行/类别/模式哈希)
|
|
125
|
+
base = f"{j['file']}:{j['line']}:{j['category']}:{j['pattern']}"
|
|
126
|
+
j["id"] = _make_issue_id(base, j["language"])
|
|
127
|
+
# 评分:confidence * severity_weight
|
|
128
|
+
j["score"] = round(j["confidence"] * _SEVERITY_WEIGHT.get(j["severity"], 1.0), 2)
|
|
129
|
+
return j
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _make_issue_id(base: str, lang: str) -> str:
|
|
133
|
+
h = hashlib.sha1(base.encode("utf-8")).hexdigest()[:6]
|
|
134
|
+
prefix = "C" if lang.startswith("c") else "R"
|
|
135
|
+
return f"{prefix}{h.upper()}"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ---------------------------
|
|
139
|
+
# 聚合与评分
|
|
140
|
+
# ---------------------------
|
|
141
|
+
|
|
142
|
+
def aggregate_issues(
|
|
143
|
+
issues: List[Union[Issue, Dict]],
|
|
144
|
+
scanned_root: Optional[str] = None,
|
|
145
|
+
scanned_files: Optional[int] = None,
|
|
146
|
+
) -> Dict:
|
|
147
|
+
"""
|
|
148
|
+
聚合问题列表并生成 JSON 报告。
|
|
149
|
+
"""
|
|
150
|
+
items = [_normalize_issue(_as_dict(it)) for it in issues]
|
|
151
|
+
|
|
152
|
+
summary: Dict = {
|
|
153
|
+
"total": len(items),
|
|
154
|
+
"by_language": {"c/cpp": 0, "rust": 0},
|
|
155
|
+
"by_category": {k: 0 for k in _CATEGORY_ORDER},
|
|
156
|
+
"top_risk_files": [],
|
|
157
|
+
}
|
|
158
|
+
if scanned_root is not None:
|
|
159
|
+
summary["scanned_root"] = scanned_root
|
|
160
|
+
if scanned_files is not None:
|
|
161
|
+
summary["scanned_files"] = scanned_files
|
|
162
|
+
|
|
163
|
+
file_score: Dict[str, float] = {}
|
|
164
|
+
for it in items:
|
|
165
|
+
lang = it["language"]
|
|
166
|
+
summary["by_language"][lang] = summary["by_language"].get(lang, 0) + 1
|
|
167
|
+
cat = it["category"]
|
|
168
|
+
summary["by_category"][cat] = summary["by_category"].get(cat, 0) + 1
|
|
169
|
+
file_score[it["file"]] = file_score.get(it["file"], 0.0) + it["score"]
|
|
170
|
+
|
|
171
|
+
# Top 风险文件按累计分排序,更稳定、可解释
|
|
172
|
+
summary["top_risk_files"] = [
|
|
173
|
+
f for f, _ in sorted(file_score.items(), key=lambda x: x[1], reverse=True)[:10]
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
report = {
|
|
177
|
+
"summary": summary,
|
|
178
|
+
"issues": items,
|
|
179
|
+
}
|
|
180
|
+
return report
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ---------------------------
|
|
184
|
+
# Markdown 渲染
|
|
185
|
+
# ---------------------------
|
|
186
|
+
|
|
187
|
+
def format_markdown_report(report_json: Dict) -> str:
|
|
188
|
+
"""
|
|
189
|
+
将聚合后的 JSON 报告渲染为 Markdown。
|
|
190
|
+
"""
|
|
191
|
+
s = report_json.get("summary", {})
|
|
192
|
+
issues: List[Dict] = report_json.get("issues", [])
|
|
193
|
+
lines: List[str] = []
|
|
194
|
+
|
|
195
|
+
lines.append("# 安全问题分析报告(聚合)")
|
|
196
|
+
lines.append("")
|
|
197
|
+
if "scanned_root" in s:
|
|
198
|
+
lines.append(f"- 扫描根目录: {s.get('scanned_root')}")
|
|
199
|
+
if "scanned_files" in s:
|
|
200
|
+
lines.append(f"- 扫描文件数: {s.get('scanned_files')}")
|
|
201
|
+
lines.append(f"- 检出问题总数: {s.get('total', 0)}")
|
|
202
|
+
lines.append("")
|
|
203
|
+
|
|
204
|
+
# 概览
|
|
205
|
+
lines.append("## 统计概览")
|
|
206
|
+
by_lang = s.get("by_language", {})
|
|
207
|
+
lines.append(f"- 按语言: c/cpp={by_lang.get('c/cpp', 0)}, rust={by_lang.get('rust', 0)}")
|
|
208
|
+
lines.append("- 按类别:")
|
|
209
|
+
by_cat = s.get("by_category", {})
|
|
210
|
+
for k in _CATEGORY_ORDER:
|
|
211
|
+
v = by_cat.get(k, 0)
|
|
212
|
+
lines.append(f" - {k}: {v}")
|
|
213
|
+
if s.get("top_risk_files"):
|
|
214
|
+
lines.append("- Top 风险文件:")
|
|
215
|
+
for f in s["top_risk_files"]:
|
|
216
|
+
lines.append(f" - {f}")
|
|
217
|
+
lines.append("")
|
|
218
|
+
|
|
219
|
+
# 详细问题
|
|
220
|
+
lines.append("## 详细问题")
|
|
221
|
+
for i, it in enumerate(issues, start=1):
|
|
222
|
+
lines.append(f"### [{i}] {it.get('file')}:{it.get('line')} ({it.get('language')}, {it.get('category')})")
|
|
223
|
+
lines.append(f"- 模式: {it.get('pattern')}")
|
|
224
|
+
lines.append(f"- 证据: `{it.get('evidence')}`")
|
|
225
|
+
lines.append(f"- 前置条件: {it.get('preconditions')}")
|
|
226
|
+
lines.append(f"- 触发路径: {it.get('trigger_path')}")
|
|
227
|
+
lines.append(f"- 后果: {it.get('consequences')}")
|
|
228
|
+
lines.append(f"- 建议: {it.get('suggestions')}")
|
|
229
|
+
lines.append(f"- 置信度: {it.get('confidence')}, 严重性: {it.get('severity')}, 评分: {it.get('score')}")
|
|
230
|
+
lines.append("")
|
|
231
|
+
|
|
232
|
+
return "\n".join(lines)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def build_json_and_markdown(
|
|
236
|
+
issues: List[Union[Issue, Dict]],
|
|
237
|
+
scanned_root: Optional[str] = None,
|
|
238
|
+
scanned_files: Optional[int] = None,
|
|
239
|
+
meta: Optional[List[Dict]] = None,
|
|
240
|
+
) -> str:
|
|
241
|
+
"""
|
|
242
|
+
一次性生成报告文本(仅 Markdown)。
|
|
243
|
+
"""
|
|
244
|
+
report = aggregate_issues(issues, scanned_root=scanned_root, scanned_files=scanned_files)
|
|
245
|
+
if meta is not None:
|
|
246
|
+
try:
|
|
247
|
+
report["meta"] = meta # 注入可选审计信息(仅用于JSON时保留,为兼容未来需要)
|
|
248
|
+
except Exception:
|
|
249
|
+
pass
|
|
250
|
+
return format_markdown_report(report)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
__all__ = [
|
|
254
|
+
"aggregate_issues",
|
|
255
|
+
"format_markdown_report",
|
|
256
|
+
"build_json_and_markdown",
|
|
257
|
+
]
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
进度状态管理模块
|
|
4
|
+
|
|
5
|
+
提供结构化的进度状态文件,准确反映当前所处的阶段和进度。
|
|
6
|
+
状态文件格式:JSON,包含当前阶段、进度百分比、已完成/总数等信息。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, Optional, Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StatusManager:
|
|
16
|
+
"""进度状态管理器"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, entry_path: str):
|
|
19
|
+
"""
|
|
20
|
+
初始化状态管理器
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
entry_path: 待分析的根目录路径(可以是项目根目录或 .jarvis/sec 目录)
|
|
24
|
+
"""
|
|
25
|
+
self.entry_path = Path(entry_path)
|
|
26
|
+
# 检查 entry_path 是否已经是 .jarvis/sec 目录
|
|
27
|
+
if self.entry_path.name == "sec" and self.entry_path.parent.name == ".jarvis":
|
|
28
|
+
sec_dir = self.entry_path
|
|
29
|
+
else:
|
|
30
|
+
sec_dir = self.entry_path / ".jarvis" / "sec"
|
|
31
|
+
self.status_path = sec_dir / "status.json"
|
|
32
|
+
self._ensure_dir()
|
|
33
|
+
|
|
34
|
+
def _ensure_dir(self):
|
|
35
|
+
"""确保状态文件目录存在"""
|
|
36
|
+
self.status_path.parent.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
|
|
38
|
+
def _read_status(self) -> Dict[str, Any]:
|
|
39
|
+
"""读取当前状态"""
|
|
40
|
+
if not self.status_path.exists():
|
|
41
|
+
return {}
|
|
42
|
+
try:
|
|
43
|
+
with self.status_path.open("r", encoding="utf-8") as f:
|
|
44
|
+
return json.load(f)
|
|
45
|
+
except Exception:
|
|
46
|
+
return {}
|
|
47
|
+
|
|
48
|
+
def _write_status(self, status: Dict[str, Any]):
|
|
49
|
+
"""写入状态文件"""
|
|
50
|
+
try:
|
|
51
|
+
status["last_updated"] = datetime.utcnow().isoformat() + "Z"
|
|
52
|
+
with self.status_path.open("w", encoding="utf-8") as f:
|
|
53
|
+
json.dump(status, f, ensure_ascii=False, indent=2)
|
|
54
|
+
except Exception:
|
|
55
|
+
# 状态文件写入失败不影响主流程
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
def update_stage(
|
|
59
|
+
self,
|
|
60
|
+
stage: str,
|
|
61
|
+
progress: Optional[float] = None,
|
|
62
|
+
current: Optional[int] = None,
|
|
63
|
+
total: Optional[int] = None,
|
|
64
|
+
message: Optional[str] = None,
|
|
65
|
+
details: Optional[Dict[str, Any]] = None,
|
|
66
|
+
):
|
|
67
|
+
"""
|
|
68
|
+
更新当前阶段和进度
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
stage: 阶段名称(pre_scan, clustering, verification, completed, error)
|
|
72
|
+
progress: 进度百分比(0-100),如果为None则根据current/total计算
|
|
73
|
+
current: 当前已完成数量
|
|
74
|
+
total: 总数量
|
|
75
|
+
message: 状态消息
|
|
76
|
+
details: 额外的详细信息
|
|
77
|
+
"""
|
|
78
|
+
status = self._read_status()
|
|
79
|
+
|
|
80
|
+
# 计算进度百分比
|
|
81
|
+
if progress is None and current is not None and total is not None and total > 0:
|
|
82
|
+
progress = (current / total) * 100
|
|
83
|
+
|
|
84
|
+
# 更新状态
|
|
85
|
+
status["stage"] = stage
|
|
86
|
+
if progress is not None:
|
|
87
|
+
status["progress"] = round(progress, 2)
|
|
88
|
+
if current is not None:
|
|
89
|
+
status["current"] = current
|
|
90
|
+
if total is not None:
|
|
91
|
+
status["total"] = total
|
|
92
|
+
if message:
|
|
93
|
+
status["message"] = message
|
|
94
|
+
if details:
|
|
95
|
+
status["details"] = details
|
|
96
|
+
|
|
97
|
+
# 设置阶段开始时间(如果是新阶段)
|
|
98
|
+
if "stage_history" not in status:
|
|
99
|
+
status["stage_history"] = []
|
|
100
|
+
|
|
101
|
+
# 检查是否是阶段切换
|
|
102
|
+
last_stage = status.get("stage")
|
|
103
|
+
if last_stage != stage:
|
|
104
|
+
status["stage_history"].append({
|
|
105
|
+
"stage": stage,
|
|
106
|
+
"started_at": datetime.utcnow().isoformat() + "Z"
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
self._write_status(status)
|
|
110
|
+
|
|
111
|
+
def update_pre_scan(
|
|
112
|
+
self,
|
|
113
|
+
current_files: Optional[int] = None,
|
|
114
|
+
total_files: Optional[int] = None,
|
|
115
|
+
issues_found: Optional[int] = None,
|
|
116
|
+
message: Optional[str] = None,
|
|
117
|
+
):
|
|
118
|
+
"""更新启发式扫描阶段状态"""
|
|
119
|
+
details = {}
|
|
120
|
+
if issues_found is not None:
|
|
121
|
+
details["issues_found"] = issues_found
|
|
122
|
+
|
|
123
|
+
progress = None
|
|
124
|
+
if current_files is not None and total_files is not None and total_files > 0:
|
|
125
|
+
progress = (current_files / total_files) * 100
|
|
126
|
+
|
|
127
|
+
self.update_stage(
|
|
128
|
+
stage="pre_scan",
|
|
129
|
+
progress=progress,
|
|
130
|
+
current=current_files,
|
|
131
|
+
total=total_files,
|
|
132
|
+
message=message or "正在进行启发式扫描...",
|
|
133
|
+
details=details,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def update_clustering(
|
|
137
|
+
self,
|
|
138
|
+
current_file: Optional[int] = None,
|
|
139
|
+
total_files: Optional[int] = None,
|
|
140
|
+
current_batch: Optional[int] = None,
|
|
141
|
+
total_batches: Optional[int] = None,
|
|
142
|
+
file_name: Optional[str] = None,
|
|
143
|
+
message: Optional[str] = None,
|
|
144
|
+
):
|
|
145
|
+
"""更新聚类阶段状态"""
|
|
146
|
+
details = {}
|
|
147
|
+
if file_name:
|
|
148
|
+
details["current_file"] = file_name
|
|
149
|
+
if current_batch is not None:
|
|
150
|
+
details["current_batch"] = current_batch
|
|
151
|
+
if total_batches is not None:
|
|
152
|
+
details["total_batches"] = total_batches
|
|
153
|
+
|
|
154
|
+
# 计算总体进度(文件级别)
|
|
155
|
+
progress = None
|
|
156
|
+
if current_file is not None and total_files is not None and total_files > 0:
|
|
157
|
+
progress = (current_file / total_files) * 100
|
|
158
|
+
|
|
159
|
+
self.update_stage(
|
|
160
|
+
stage="clustering",
|
|
161
|
+
progress=progress,
|
|
162
|
+
current=current_file,
|
|
163
|
+
total=total_files,
|
|
164
|
+
message=message or "正在进行聚类分析...",
|
|
165
|
+
details=details,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def update_review(
|
|
169
|
+
self,
|
|
170
|
+
current_review: Optional[int] = None,
|
|
171
|
+
total_reviews: Optional[int] = None,
|
|
172
|
+
message: Optional[str] = None,
|
|
173
|
+
):
|
|
174
|
+
"""更新复核阶段状态"""
|
|
175
|
+
details = {}
|
|
176
|
+
|
|
177
|
+
# 计算总体进度
|
|
178
|
+
progress = None
|
|
179
|
+
if current_review is not None and total_reviews is not None and total_reviews > 0:
|
|
180
|
+
progress = (current_review / total_reviews) * 100
|
|
181
|
+
|
|
182
|
+
self.update_stage(
|
|
183
|
+
stage="review",
|
|
184
|
+
progress=progress,
|
|
185
|
+
current=current_review,
|
|
186
|
+
total=total_reviews,
|
|
187
|
+
message=message or "正在进行无效聚类复核...",
|
|
188
|
+
details=details,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def update_verification(
|
|
192
|
+
self,
|
|
193
|
+
current_batch: Optional[int] = None,
|
|
194
|
+
total_batches: Optional[int] = None,
|
|
195
|
+
current_task: Optional[int] = None,
|
|
196
|
+
total_tasks: Optional[int] = None,
|
|
197
|
+
batch_id: Optional[str] = None,
|
|
198
|
+
file_name: Optional[str] = None,
|
|
199
|
+
issues_found: Optional[int] = None,
|
|
200
|
+
message: Optional[str] = None,
|
|
201
|
+
):
|
|
202
|
+
"""更新验证阶段状态"""
|
|
203
|
+
details = {}
|
|
204
|
+
if batch_id:
|
|
205
|
+
details["batch_id"] = batch_id
|
|
206
|
+
if file_name:
|
|
207
|
+
details["file"] = file_name
|
|
208
|
+
if issues_found is not None:
|
|
209
|
+
details["issues_found"] = issues_found
|
|
210
|
+
|
|
211
|
+
# 计算总体进度(批次级别)
|
|
212
|
+
progress = None
|
|
213
|
+
if current_batch is not None and total_batches is not None and total_batches > 0:
|
|
214
|
+
progress = (current_batch / total_batches) * 100
|
|
215
|
+
|
|
216
|
+
self.update_stage(
|
|
217
|
+
stage="verification",
|
|
218
|
+
progress=progress,
|
|
219
|
+
current=current_batch,
|
|
220
|
+
total=total_batches,
|
|
221
|
+
message=message or "正在进行安全验证...",
|
|
222
|
+
details=details,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def mark_completed(
|
|
226
|
+
self,
|
|
227
|
+
total_issues: Optional[int] = None,
|
|
228
|
+
message: Optional[str] = None,
|
|
229
|
+
):
|
|
230
|
+
"""标记分析完成"""
|
|
231
|
+
details = {}
|
|
232
|
+
if total_issues is not None:
|
|
233
|
+
details["total_issues"] = total_issues
|
|
234
|
+
|
|
235
|
+
self.update_stage(
|
|
236
|
+
stage="completed",
|
|
237
|
+
progress=100.0,
|
|
238
|
+
message=message or "安全分析已完成",
|
|
239
|
+
details=details,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def mark_error(
|
|
243
|
+
self,
|
|
244
|
+
error_message: str,
|
|
245
|
+
error_type: Optional[str] = None,
|
|
246
|
+
):
|
|
247
|
+
"""标记错误状态"""
|
|
248
|
+
details = {"error_message": error_message}
|
|
249
|
+
if error_type:
|
|
250
|
+
details["error_type"] = error_type
|
|
251
|
+
|
|
252
|
+
self.update_stage(
|
|
253
|
+
stage="error",
|
|
254
|
+
message=f"发生错误: {error_message}",
|
|
255
|
+
details=details,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def get_status(self) -> Dict[str, Any]:
|
|
259
|
+
"""获取当前状态"""
|
|
260
|
+
return self._read_status()
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
__all__ = ["StatusManager"]
|
|
264
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Shared types for jarvis.jarvis_sec to avoid circular imports.
|
|
4
|
+
"""
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Issue:
|
|
9
|
+
language: str
|
|
10
|
+
category: str
|
|
11
|
+
pattern: str
|
|
12
|
+
file: str
|
|
13
|
+
line: int
|
|
14
|
+
evidence: str
|
|
15
|
+
description: str
|
|
16
|
+
suggestion: str
|
|
17
|
+
confidence: float
|
|
18
|
+
severity: str = "medium"
|
|
19
|
+
|
|
20
|
+
__all__ = ["Issue"]
|