jarvis-ai-assistant 0.7.0__py3-none-any.whl → 0.7.6__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 (159) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +243 -139
  3. jarvis/jarvis_agent/agent_manager.py +5 -10
  4. jarvis/jarvis_agent/builtin_input_handler.py +2 -6
  5. jarvis/jarvis_agent/config_editor.py +2 -7
  6. jarvis/jarvis_agent/event_bus.py +82 -12
  7. jarvis/jarvis_agent/file_context_handler.py +265 -15
  8. jarvis/jarvis_agent/file_methodology_manager.py +3 -4
  9. jarvis/jarvis_agent/jarvis.py +113 -98
  10. jarvis/jarvis_agent/language_extractors/__init__.py +57 -0
  11. jarvis/jarvis_agent/language_extractors/c_extractor.py +21 -0
  12. jarvis/jarvis_agent/language_extractors/cpp_extractor.py +21 -0
  13. jarvis/jarvis_agent/language_extractors/go_extractor.py +21 -0
  14. jarvis/jarvis_agent/language_extractors/java_extractor.py +84 -0
  15. jarvis/jarvis_agent/language_extractors/javascript_extractor.py +79 -0
  16. jarvis/jarvis_agent/language_extractors/python_extractor.py +21 -0
  17. jarvis/jarvis_agent/language_extractors/rust_extractor.py +21 -0
  18. jarvis/jarvis_agent/language_extractors/typescript_extractor.py +84 -0
  19. jarvis/jarvis_agent/language_support_info.py +486 -0
  20. jarvis/jarvis_agent/main.py +6 -12
  21. jarvis/jarvis_agent/memory_manager.py +7 -16
  22. jarvis/jarvis_agent/methodology_share_manager.py +10 -16
  23. jarvis/jarvis_agent/prompt_manager.py +1 -1
  24. jarvis/jarvis_agent/prompts.py +193 -171
  25. jarvis/jarvis_agent/protocols.py +8 -12
  26. jarvis/jarvis_agent/run_loop.py +77 -14
  27. jarvis/jarvis_agent/session_manager.py +2 -3
  28. jarvis/jarvis_agent/share_manager.py +12 -21
  29. jarvis/jarvis_agent/shell_input_handler.py +1 -2
  30. jarvis/jarvis_agent/task_analyzer.py +26 -4
  31. jarvis/jarvis_agent/task_manager.py +11 -27
  32. jarvis/jarvis_agent/tool_executor.py +2 -3
  33. jarvis/jarvis_agent/tool_share_manager.py +12 -24
  34. jarvis/jarvis_agent/web_server.py +55 -20
  35. jarvis/jarvis_c2rust/__init__.py +5 -5
  36. jarvis/jarvis_c2rust/cli.py +461 -499
  37. jarvis/jarvis_c2rust/collector.py +45 -53
  38. jarvis/jarvis_c2rust/constants.py +26 -0
  39. jarvis/jarvis_c2rust/library_replacer.py +264 -132
  40. jarvis/jarvis_c2rust/llm_module_agent.py +162 -190
  41. jarvis/jarvis_c2rust/loaders.py +207 -0
  42. jarvis/jarvis_c2rust/models.py +28 -0
  43. jarvis/jarvis_c2rust/optimizer.py +1592 -395
  44. jarvis/jarvis_c2rust/transpiler.py +1722 -1064
  45. jarvis/jarvis_c2rust/utils.py +385 -0
  46. jarvis/jarvis_code_agent/build_validation_config.py +2 -3
  47. jarvis/jarvis_code_agent/code_agent.py +394 -320
  48. jarvis/jarvis_code_agent/code_analyzer/__init__.py +3 -0
  49. jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +4 -0
  50. jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +17 -2
  51. jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +3 -0
  52. jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +36 -4
  53. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +9 -0
  54. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +9 -0
  55. jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +12 -1
  56. jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +22 -5
  57. jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +57 -32
  58. jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +62 -6
  59. jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +8 -9
  60. jarvis/jarvis_code_agent/code_analyzer/context_manager.py +290 -5
  61. jarvis/jarvis_code_agent/code_analyzer/language_support.py +21 -0
  62. jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +21 -3
  63. jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +72 -4
  64. jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +35 -3
  65. jarvis/jarvis_code_agent/code_analyzer/languages/java_language.py +212 -0
  66. jarvis/jarvis_code_agent/code_analyzer/languages/javascript_language.py +254 -0
  67. jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +52 -2
  68. jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +73 -1
  69. jarvis/jarvis_code_agent/code_analyzer/languages/typescript_language.py +280 -0
  70. jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +306 -152
  71. jarvis/jarvis_code_agent/code_analyzer/structured_code.py +556 -0
  72. jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +193 -18
  73. jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +18 -8
  74. jarvis/jarvis_code_agent/lint.py +258 -27
  75. jarvis/jarvis_code_agent/utils.py +0 -1
  76. jarvis/jarvis_code_analysis/code_review.py +19 -24
  77. jarvis/jarvis_data/config_schema.json +53 -26
  78. jarvis/jarvis_git_squash/main.py +4 -5
  79. jarvis/jarvis_git_utils/git_commiter.py +44 -49
  80. jarvis/jarvis_mcp/sse_mcp_client.py +20 -27
  81. jarvis/jarvis_mcp/stdio_mcp_client.py +11 -12
  82. jarvis/jarvis_mcp/streamable_mcp_client.py +15 -14
  83. jarvis/jarvis_memory_organizer/memory_organizer.py +55 -74
  84. jarvis/jarvis_methodology/main.py +32 -48
  85. jarvis/jarvis_multi_agent/__init__.py +79 -61
  86. jarvis/jarvis_multi_agent/main.py +3 -7
  87. jarvis/jarvis_platform/base.py +469 -199
  88. jarvis/jarvis_platform/human.py +7 -8
  89. jarvis/jarvis_platform/kimi.py +30 -36
  90. jarvis/jarvis_platform/openai.py +65 -27
  91. jarvis/jarvis_platform/registry.py +26 -10
  92. jarvis/jarvis_platform/tongyi.py +24 -25
  93. jarvis/jarvis_platform/yuanbao.py +31 -42
  94. jarvis/jarvis_platform_manager/main.py +66 -77
  95. jarvis/jarvis_platform_manager/service.py +8 -13
  96. jarvis/jarvis_rag/cli.py +49 -51
  97. jarvis/jarvis_rag/embedding_manager.py +13 -18
  98. jarvis/jarvis_rag/llm_interface.py +8 -9
  99. jarvis/jarvis_rag/query_rewriter.py +10 -21
  100. jarvis/jarvis_rag/rag_pipeline.py +24 -27
  101. jarvis/jarvis_rag/reranker.py +4 -5
  102. jarvis/jarvis_rag/retriever.py +28 -30
  103. jarvis/jarvis_sec/__init__.py +220 -3520
  104. jarvis/jarvis_sec/agents.py +143 -0
  105. jarvis/jarvis_sec/analysis.py +276 -0
  106. jarvis/jarvis_sec/cli.py +29 -6
  107. jarvis/jarvis_sec/clustering.py +1439 -0
  108. jarvis/jarvis_sec/file_manager.py +427 -0
  109. jarvis/jarvis_sec/parsers.py +73 -0
  110. jarvis/jarvis_sec/prompts.py +268 -0
  111. jarvis/jarvis_sec/report.py +83 -4
  112. jarvis/jarvis_sec/review.py +453 -0
  113. jarvis/jarvis_sec/utils.py +499 -0
  114. jarvis/jarvis_sec/verification.py +848 -0
  115. jarvis/jarvis_sec/workflow.py +7 -0
  116. jarvis/jarvis_smart_shell/main.py +38 -87
  117. jarvis/jarvis_stats/cli.py +1 -1
  118. jarvis/jarvis_stats/stats.py +7 -7
  119. jarvis/jarvis_stats/storage.py +15 -21
  120. jarvis/jarvis_tools/clear_memory.py +3 -20
  121. jarvis/jarvis_tools/cli/main.py +20 -23
  122. jarvis/jarvis_tools/edit_file.py +1066 -0
  123. jarvis/jarvis_tools/execute_script.py +42 -21
  124. jarvis/jarvis_tools/file_analyzer.py +6 -9
  125. jarvis/jarvis_tools/generate_new_tool.py +11 -20
  126. jarvis/jarvis_tools/lsp_client.py +1552 -0
  127. jarvis/jarvis_tools/methodology.py +2 -3
  128. jarvis/jarvis_tools/read_code.py +1525 -87
  129. jarvis/jarvis_tools/read_symbols.py +2 -3
  130. jarvis/jarvis_tools/read_webpage.py +7 -10
  131. jarvis/jarvis_tools/registry.py +370 -181
  132. jarvis/jarvis_tools/retrieve_memory.py +20 -19
  133. jarvis/jarvis_tools/rewrite_file.py +105 -0
  134. jarvis/jarvis_tools/save_memory.py +3 -15
  135. jarvis/jarvis_tools/search_web.py +3 -7
  136. jarvis/jarvis_tools/sub_agent.py +17 -6
  137. jarvis/jarvis_tools/sub_code_agent.py +14 -16
  138. jarvis/jarvis_tools/virtual_tty.py +54 -32
  139. jarvis/jarvis_utils/clipboard.py +7 -10
  140. jarvis/jarvis_utils/config.py +98 -63
  141. jarvis/jarvis_utils/embedding.py +5 -5
  142. jarvis/jarvis_utils/fzf.py +8 -8
  143. jarvis/jarvis_utils/git_utils.py +81 -67
  144. jarvis/jarvis_utils/input.py +24 -49
  145. jarvis/jarvis_utils/jsonnet_compat.py +465 -0
  146. jarvis/jarvis_utils/methodology.py +33 -35
  147. jarvis/jarvis_utils/utils.py +245 -202
  148. {jarvis_ai_assistant-0.7.0.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/METADATA +205 -70
  149. jarvis_ai_assistant-0.7.6.dist-info/RECORD +218 -0
  150. jarvis/jarvis_agent/edit_file_handler.py +0 -584
  151. jarvis/jarvis_agent/rewrite_file_handler.py +0 -141
  152. jarvis/jarvis_agent/task_planner.py +0 -496
  153. jarvis/jarvis_platform/ai8.py +0 -332
  154. jarvis/jarvis_tools/ask_user.py +0 -54
  155. jarvis_ai_assistant-0.7.0.dist-info/RECORD +0 -192
  156. {jarvis_ai_assistant-0.7.0.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/WHEEL +0 -0
  157. {jarvis_ai_assistant-0.7.0.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/entry_points.txt +0 -0
  158. {jarvis_ai_assistant-0.7.0.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/licenses/LICENSE +0 -0
  159. {jarvis_ai_assistant-0.7.0.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,143 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Agent创建和订阅模块"""
3
+
4
+ from typing import Dict, Optional
5
+ from jarvis.jarvis_agent import Agent
6
+ from jarvis.jarvis_tools.registry import ToolRegistry
7
+ from jarvis.jarvis_sec.prompts import (
8
+ build_summary_prompt,
9
+ get_review_system_prompt,
10
+ get_review_summary_prompt,
11
+ get_cluster_system_prompt,
12
+ get_cluster_summary_prompt,
13
+ )
14
+
15
+
16
+ def subscribe_summary_event(agent: Agent) -> Dict[str, str]:
17
+ """订阅Agent摘要事件"""
18
+ summary_container: Dict[str, str] = {"text": ""}
19
+ try:
20
+ from jarvis.jarvis_agent.events import AFTER_SUMMARY as _AFTER_SUMMARY
21
+ except Exception:
22
+ _AFTER_SUMMARY = None
23
+
24
+ if _AFTER_SUMMARY:
25
+ def _on_after_summary(**kwargs):
26
+ try:
27
+ summary_container["text"] = str(kwargs.get("summary", "") or "")
28
+ except Exception:
29
+ summary_container["text"] = ""
30
+ try:
31
+ agent.event_bus.subscribe(_AFTER_SUMMARY, _on_after_summary)
32
+ except Exception:
33
+ pass
34
+ return summary_container
35
+
36
+
37
+ def create_analysis_agent(task_id: str, llm_group: Optional[str], force_save_memory: bool = False) -> Agent:
38
+ """创建分析Agent"""
39
+ system_prompt = """
40
+ # 单Agent安全分析约束
41
+ - 你的核心任务是评估代码的安全问题,目标:针对本候选问题进行证据核实、风险评估与修复建议补充,查找漏洞触发路径,确认在某些条件下会触发;以此来判断是否是漏洞。
42
+ - **必须进行调用路径推导**:
43
+ - 对于每个候选问题,必须明确推导从可控输入到缺陷代码的完整调用路径。
44
+ - 调用路径推导必须包括:
45
+ 1. 识别可控输入的来源(例如:用户输入、网络数据、文件读取、命令行参数等)
46
+ 2. 追踪数据流:从输入源开始,逐步追踪数据如何传递到缺陷代码位置
47
+ 3. 识别调用链:明确列出从入口函数到缺陷代码的所有函数调用序列(例如:main() -> parse_input() -> process_data() -> vulnerable_function())
48
+ 4. 分析每个调用点的数据校验情况:检查每个函数是否对输入进行了校验、边界检查或安全检查
49
+ 5. 确认触发条件:明确说明在什么条件下,未校验或恶意输入能够到达缺陷代码位置
50
+ - 如果无法推导出完整的调用路径,或者所有调用路径都有充分的保护措施,则应该判定为误报。
51
+ - 调用路径推导必须在分析过程中明确展示,不能省略或假设。
52
+ - 工具优先:使用 read_code 读取目标文件附近源码(行号前后各 ~50 行),必要时用 execute_script 辅助检索。
53
+ - **调用路径追溯要求**:
54
+ - 必须向上追溯所有可能的调用者,查看完整的调用路径,以确认风险是否真实存在。
55
+ - 使用 read_code 和 execute_script 工具查找函数的调用者(例如:使用 grep 搜索函数名,查找所有调用该函数的位置)。
56
+ - 对于每个调用者,必须检查其是否对输入进行了校验。
57
+ - 如果发现任何调用路径未做校验,必须明确记录该路径。
58
+ - 例如:一个函数存在空指针解引用风险,必须检查所有调用者。如果所有调用者均能确保传入的指针非空,则该风险在当前代码库中可能不会实际触发;但如果存在任何调用者未做校验,则风险真实存在。
59
+ - 若多条告警位于同一文件且行号相距不远,可一次性读取共享上下文,对这些相邻告警进行联合分析与判断;但仍需避免无关扩展与大范围遍历。
60
+ - 禁止修改任何文件或执行写操作命令(rm/mv/cp/echo >、sed -i、git、patch、chmod、chown 等);仅进行只读分析与读取。
61
+ - 每次仅执行一个操作;等待工具结果后再进行下一步。
62
+ - **记忆使用**:
63
+ - 在分析过程中,充分利用 retrieve_memory 工具检索已有的记忆,特别是与当前分析函数相关的记忆。
64
+ - 如果有必要,使用 save_memory 工具保存每个函数的分析要点,使用函数名作为 tag(例如:函数名、文件名等)。
65
+ - 记忆内容示例:某个函数的指针已经判空、某个函数已有输入校验、某个函数的调用路径分析结果等。
66
+ - 这样可以避免重复分析,提高效率,并保持分析的一致性。
67
+ - 完成对本批次候选问题的判断后,主输出仅打印结束符 <!!!COMPLETE!!!> ,不需要汇总结果。
68
+ """.strip()
69
+
70
+ agent_kwargs: Dict = dict(
71
+ system_prompt=system_prompt,
72
+ name=task_id,
73
+ auto_complete=True,
74
+ need_summary=True,
75
+ summary_prompt=build_summary_prompt(),
76
+ non_interactive=True,
77
+ in_multi_agent=False,
78
+ use_methodology=False,
79
+ use_analysis=False,
80
+ output_handler=[ToolRegistry()],
81
+ force_save_memory=force_save_memory,
82
+ use_tools=["read_code", "execute_script", "save_memory", "retrieve_memory"],
83
+ )
84
+ if llm_group:
85
+ agent_kwargs["model_group"] = llm_group
86
+ return Agent(**agent_kwargs)
87
+
88
+
89
+ def create_review_agent(
90
+ current_review_num: int,
91
+ llm_group: Optional[str],
92
+ ) -> Agent:
93
+ """创建复核Agent"""
94
+ review_system_prompt = get_review_system_prompt()
95
+ review_summary_prompt = get_review_summary_prompt()
96
+
97
+ review_task_id = f"JARVIS-SEC-Review-Batch-{current_review_num}"
98
+ review_agent_kwargs: Dict = dict(
99
+ system_prompt=review_system_prompt,
100
+ name=review_task_id,
101
+ auto_complete=True,
102
+ need_summary=True,
103
+ summary_prompt=review_summary_prompt,
104
+ non_interactive=True,
105
+ in_multi_agent=False,
106
+ use_methodology=False,
107
+ use_analysis=False,
108
+ output_handler=[ToolRegistry()],
109
+ use_tools=["read_code", "execute_script", "retrieve_memory", "save_memory"],
110
+ )
111
+ if llm_group:
112
+ review_agent_kwargs["model_group"] = llm_group
113
+ return Agent(**review_agent_kwargs)
114
+
115
+
116
+ def create_cluster_agent(
117
+ file: str,
118
+ chunk_idx: int,
119
+ llm_group: Optional[str],
120
+ force_save_memory: bool = False,
121
+ ) -> Agent:
122
+ """创建聚类Agent"""
123
+ cluster_system_prompt = get_cluster_system_prompt()
124
+ cluster_summary_prompt = get_cluster_summary_prompt()
125
+
126
+ agent_kwargs_cluster: Dict = dict(
127
+ system_prompt=cluster_system_prompt,
128
+ name=f"JARVIS-SEC-Cluster::{file}::batch{chunk_idx}",
129
+ auto_complete=True,
130
+ need_summary=True,
131
+ summary_prompt=cluster_summary_prompt,
132
+ non_interactive=True,
133
+ in_multi_agent=False,
134
+ use_methodology=False,
135
+ use_analysis=False,
136
+ output_handler=[ToolRegistry()],
137
+ force_save_memory=force_save_memory,
138
+ use_tools=["read_code", "execute_script", "save_memory", "retrieve_memory"],
139
+ )
140
+ if llm_group:
141
+ agent_kwargs_cluster["model_group"] = llm_group
142
+ return Agent(**agent_kwargs_cluster)
143
+
@@ -0,0 +1,276 @@
1
+ # -*- coding: utf-8 -*-
2
+ """分析相关模块"""
3
+
4
+ from typing import Dict, List, Optional
5
+ import typer
6
+
7
+ from jarvis.jarvis_agent import Agent
8
+ from jarvis.jarvis_sec.prompts import build_summary_prompt
9
+ from jarvis.jarvis_sec.parsers import try_parse_summary_report
10
+ from jarvis.jarvis_sec.utils import git_restore_if_dirty
11
+
12
+
13
+ def valid_items(items: Optional[List]) -> bool:
14
+ """验证分析结果项的格式"""
15
+ if not isinstance(items, list):
16
+ return False
17
+ for it in items:
18
+ if not isinstance(it, dict):
19
+ return False
20
+ has_gid = "gid" in it
21
+ has_gids = "gids" in it
22
+ if not has_gid and not has_gids:
23
+ return False
24
+ if has_gid and has_gids:
25
+ return False
26
+ if has_gid:
27
+ try:
28
+ if int(it["gid"]) < 1:
29
+ return False
30
+ except Exception:
31
+ return False
32
+ elif has_gids:
33
+ if not isinstance(it["gids"], list) or len(it["gids"]) == 0:
34
+ return False
35
+ for gid_val in it["gids"]:
36
+ try:
37
+ if int(gid_val) < 1:
38
+ return False
39
+ except Exception:
40
+ return False
41
+ if "has_risk" not in it or not isinstance(it["has_risk"], bool):
42
+ return False
43
+ if it.get("has_risk"):
44
+ for key in ["preconditions", "trigger_path", "consequences", "suggestions"]:
45
+ if key not in it:
46
+ return False
47
+ if not isinstance(it[key], str) or not it[key].strip():
48
+ return False
49
+ return True
50
+
51
+
52
+ def build_analysis_task_context(batch: List[Dict], entry_path: str, langs: List[str]) -> str:
53
+ """构建分析任务上下文"""
54
+ import json as _json2
55
+ batch_ctx: List[Dict] = list(batch)
56
+ cluster_verify = str(batch_ctx[0].get("verify") if batch_ctx else "")
57
+ cluster_gids_ctx = [it.get("gid") for it in batch_ctx]
58
+ return f"""
59
+ # 安全子任务批次
60
+ 上下文参数:
61
+ - entry_path: {entry_path}
62
+ - languages: {langs}
63
+ - cluster_verification: {cluster_verify}
64
+
65
+ - cluster_gids: {cluster_gids_ctx}
66
+ - note: 每个候选含 gid/verify 字段,模型仅需输出 gid 统一给出验证/判断结论(全局编号);无需使用局部 id
67
+
68
+ 批次候选(JSON数组):
69
+ {_json2.dumps(batch_ctx, ensure_ascii=False, indent=2)}
70
+ """.strip()
71
+
72
+
73
+ def build_validation_error_guidance(
74
+ parse_error_analysis: Optional[str],
75
+ prev_parsed_items: Optional[List],
76
+ ) -> str:
77
+ """构建验证错误指导信息"""
78
+ if parse_error_analysis:
79
+ return f"\n\n**格式错误详情(请根据以下错误修复输出格式):**\n- JSON解析失败: {parse_error_analysis}\n\n请确保输出的JSON格式正确,包括正确的引号、逗号、大括号等。仅输出一个 <REPORT> 块,块内直接包含 JSON 数组(不需要额外的标签)。支持jsonnet语法(如尾随逗号、注释、||| 或 ``` 分隔符多行字符串等)。"
80
+ elif prev_parsed_items is None:
81
+ return "\n\n**格式错误详情(请根据以下错误修复输出格式):**\n- 无法从摘要中解析出有效的 JSON 数组"
82
+ elif not valid_items(prev_parsed_items):
83
+ validation_errors = []
84
+ if not isinstance(prev_parsed_items, list):
85
+ validation_errors.append("结果不是数组")
86
+ else:
87
+ for idx, it in enumerate(prev_parsed_items):
88
+ if not isinstance(it, dict):
89
+ validation_errors.append(f"元素{idx}不是字典")
90
+ break
91
+ has_gid = "gid" in it
92
+ has_gids = "gids" in it
93
+ if not has_gid and not has_gids:
94
+ validation_errors.append(f"元素{idx}缺少必填字段 gid 或 gids")
95
+ break
96
+ if has_gid and has_gids:
97
+ validation_errors.append(f"元素{idx}不能同时包含 gid 和 gids")
98
+ break
99
+ if has_gid:
100
+ try:
101
+ if int(it.get("gid", 0)) < 1:
102
+ validation_errors.append(f"元素{idx}的 gid 必须 >= 1")
103
+ break
104
+ except Exception:
105
+ validation_errors.append(f"元素{idx}的 gid 格式错误(必须是整数)")
106
+ break
107
+ elif has_gids:
108
+ if not isinstance(it.get("gids"), list) or len(it.get("gids", [])) == 0:
109
+ validation_errors.append(f"元素{idx}的 gids 必须是非空数组")
110
+ break
111
+ try:
112
+ for gid_idx, gid_val in enumerate(it.get("gids", [])):
113
+ if int(gid_val) < 1:
114
+ validation_errors.append(f"元素{idx}的 gids[{gid_idx}] 必须 >= 1")
115
+ break
116
+ if validation_errors:
117
+ break
118
+ except Exception:
119
+ validation_errors.append(f"元素{idx}的 gids 格式错误(必须是整数数组)")
120
+ break
121
+ if "has_risk" not in it or not isinstance(it.get("has_risk"), bool):
122
+ validation_errors.append(f"元素{idx}缺少必填字段 has_risk(必须是布尔值)")
123
+ break
124
+ if it.get("has_risk"):
125
+ for key in ["preconditions", "trigger_path", "consequences", "suggestions"]:
126
+ if key not in it:
127
+ validation_errors.append(f"元素{idx}的 has_risk 为 true,但缺少必填字段 {key}")
128
+ break
129
+ if not isinstance(it[key], str) or not it[key].strip():
130
+ validation_errors.append(f"元素{idx}的 {key} 字段不能为空")
131
+ break
132
+ if validation_errors:
133
+ break
134
+ if validation_errors:
135
+ return "\n\n**格式错误详情(请根据以下错误修复输出格式):**\n" + "\n".join(f"- {err}" for err in validation_errors)
136
+ return ""
137
+
138
+
139
+ def run_analysis_agent_with_retry(
140
+ agent: Agent,
141
+ per_task: str,
142
+ summary_container: Dict[str, str],
143
+ entry_path: str,
144
+ task_id: str,
145
+ bidx: int,
146
+ meta_records: List[Dict],
147
+ ) -> tuple[Optional[List[Dict]], Optional[Dict]]:
148
+ """运行分析Agent并重试直到成功"""
149
+ summary_items: Optional[List[Dict]] = None
150
+ workspace_restore_info: Optional[Dict] = None
151
+ use_direct_model_analysis = False
152
+ prev_parsed_items: Optional[List] = None
153
+ parse_error_analysis: Optional[str] = None
154
+ attempt = 0
155
+
156
+ while True:
157
+ attempt += 1
158
+ summary_container["text"] = ""
159
+
160
+ if use_direct_model_analysis:
161
+ summary_prompt_text = build_summary_prompt()
162
+ error_guidance = build_validation_error_guidance(parse_error_analysis, prev_parsed_items)
163
+ full_prompt = f"{per_task}{error_guidance}\n\n{summary_prompt_text}"
164
+ try:
165
+ response = agent.model.chat_until_success(full_prompt) # type: ignore
166
+ summary_container["text"] = response
167
+ except Exception as e:
168
+ try:
169
+ typer.secho(f"[jarvis-sec] 直接模型调用失败: {e},回退到 run()", fg=typer.colors.YELLOW)
170
+ except Exception:
171
+ pass
172
+ agent.run(per_task)
173
+ else:
174
+ agent.run(per_task)
175
+
176
+ # 工作区保护
177
+ try:
178
+ _changed = git_restore_if_dirty(entry_path)
179
+ workspace_restore_info = {
180
+ "performed": bool(_changed),
181
+ "changed_files_count": int(_changed or 0),
182
+ "action": "git checkout -- .",
183
+ }
184
+ meta_records.append({
185
+ "task_id": task_id,
186
+ "batch_index": bidx,
187
+ "workspace_restore": workspace_restore_info,
188
+ "attempt": attempt + 1,
189
+ })
190
+ if _changed:
191
+ try:
192
+ typer.secho(f"[jarvis-sec] 工作区已恢复 ({_changed} 个文件),操作: git checkout -- .", fg=typer.colors.BLUE)
193
+ except Exception:
194
+ pass
195
+ except Exception:
196
+ pass
197
+
198
+ # 解析摘要中的 <REPORT>(JSON)
199
+ summary_text = summary_container.get("text", "")
200
+ parsed_items: Optional[List] = None
201
+ parse_error_analysis = None
202
+ if summary_text:
203
+ rep, parse_error_analysis = try_parse_summary_report(summary_text)
204
+ if parse_error_analysis:
205
+ try:
206
+ typer.secho(f"[jarvis-sec] 分析结果JSON解析失败: {parse_error_analysis}", fg=typer.colors.YELLOW)
207
+ except Exception:
208
+ pass
209
+ elif isinstance(rep, list):
210
+ parsed_items = rep
211
+ elif isinstance(rep, dict):
212
+ items = rep.get("issues")
213
+ if isinstance(items, list):
214
+ parsed_items = items
215
+
216
+ # 关键字段校验
217
+ # 空数组 [] 是有效的(表示没有发现问题),需要单独处理
218
+ if parsed_items is not None:
219
+ if len(parsed_items) == 0:
220
+ # 空数组表示没有发现问题,这是有效的格式
221
+ summary_items = parsed_items
222
+ break
223
+ elif valid_items(parsed_items):
224
+ # 非空数组需要验证格式
225
+ summary_items = parsed_items
226
+ break
227
+
228
+ # 格式校验失败,后续重试使用直接模型调用
229
+ use_direct_model_analysis = True
230
+ prev_parsed_items = parsed_items
231
+ if parse_error_analysis:
232
+ try:
233
+ typer.secho(f"[jarvis-sec] 分析结果JSON解析失败 -> 重试第 {attempt} 次 (批次={bidx},使用直接模型调用,将反馈解析错误)", fg=typer.colors.YELLOW)
234
+ except Exception:
235
+ pass
236
+ else:
237
+ try:
238
+ typer.secho(f"[jarvis-sec] 分析结果格式无效 -> 重试第 {attempt} 次 (批次={bidx},使用直接模型调用)", fg=typer.colors.YELLOW)
239
+ except Exception:
240
+ pass
241
+
242
+ return summary_items, workspace_restore_info
243
+
244
+
245
+ def expand_and_filter_analysis_results(summary_items: List[Dict]) -> tuple[List[Dict], List[Dict]]:
246
+ """展开gids格式为单个gid格式,并过滤出有风险的项目"""
247
+ items_with_risk: List[Dict] = []
248
+ items_without_risk: List[Dict] = []
249
+ merged_items: List[Dict] = []
250
+
251
+ for it in summary_items:
252
+ has_risk = it.get("has_risk") is True
253
+ if "gids" in it and isinstance(it.get("gids"), list):
254
+ for gid_val in it.get("gids", []):
255
+ try:
256
+ gid_int = int(gid_val)
257
+ if gid_int >= 1:
258
+ item = {
259
+ **{k: v for k, v in it.items() if k != "gids"},
260
+ "gid": gid_int,
261
+ }
262
+ if has_risk:
263
+ merged_items.append(item)
264
+ items_with_risk.append(item)
265
+ else:
266
+ items_without_risk.append(item)
267
+ except Exception:
268
+ pass
269
+ elif "gid" in it:
270
+ if has_risk:
271
+ merged_items.append(it)
272
+ items_with_risk.append(it)
273
+ else:
274
+ items_without_risk.append(it)
275
+
276
+ return items_with_risk, items_without_risk
jarvis/jarvis_sec/cli.py CHANGED
@@ -5,10 +5,12 @@ Jarvis 安全演进套件 —— 命令行入口(Typer 版本)
5
5
  用法示例:
6
6
  - Agent模式(单Agent,逐条子任务分析)
7
7
  python -m jarvis.jarvis_sec.cli agent --path ./target_project
8
+ python -m jarvis.jarvis_sec.cli agent # 默认分析当前目录
8
9
 
9
10
  可选参数:
10
11
 
11
- - --output: 最终Markdown报告输出路径(默认 ./report.md)
12
+ - --path, -p: 待分析的根目录(默认当前目录)
13
+ - --output, -o: 最终报告输出路径(默认 ./report.md)。如果后缀为 .csv,则输出 CSV 格式;否则输出 Markdown 格式
12
14
  """
13
15
 
14
16
  from __future__ import annotations
@@ -20,7 +22,8 @@ from typing import Optional
20
22
  import typer
21
23
  from jarvis.jarvis_utils.utils import init_env
22
24
  # removed: set_config import(避免全局覆盖模型组配置)
23
- from jarvis.jarvis_sec.workflow import run_with_agent, direct_scan, format_markdown_report
25
+ from jarvis.jarvis_sec.workflow import run_with_agent, direct_scan, format_markdown_report as format_markdown_report_workflow
26
+ from jarvis.jarvis_sec.report import format_csv_report, aggregate_issues
24
27
 
25
28
  app = typer.Typer(
26
29
  add_completion=False,
@@ -33,22 +36,28 @@ app = typer.Typer(
33
36
 
34
37
  @app.command("agent", help="Agent模式(单Agent逐条子任务分析)")
35
38
  def agent(
36
- path: str = typer.Option(..., "--path", "-p", help="待分析的根目录"),
39
+ path: str = typer.Option(".", "--path", "-p", help="待分析的根目录(默认当前目录)"),
37
40
 
38
41
  llm_group: Optional[str] = typer.Option(
39
42
  None, "--llm-group", "-g", help="使用的模型组(仅对本次运行生效,不修改全局配置)"
40
43
  ),
41
44
  output: Optional[str] = typer.Option(
42
- "report.md", "--output", "-o", help="最终Markdown报告输出路径(默认 ./report.md"
45
+ "report.md", "--output", "-o", help="最终报告输出路径(默认 ./report.md)。如果后缀为 .csv,则输出 CSV 格式;否则输出 Markdown 格式"
43
46
  ),
44
47
 
45
48
  cluster_limit: int = typer.Option(
46
49
  50, "--cluster-limit", "-c", help="聚类每批最多处理的告警数(按文件分批聚类,默认50)"
47
50
  ),
51
+ enable_verification: bool = typer.Option(
52
+ True, "--enable-verification/--no-verification", help="是否启用二次验证(默认开启)"
53
+ ),
54
+ force_save_memory: bool = typer.Option(
55
+ False, "--force-save-memory/--no-force-save-memory", help="强制使用记忆(默认关闭)"
56
+ ),
48
57
  ) -> None:
49
58
  # 初始化环境,确保平台/模型等全局配置就绪(避免 NoneType 平台)
50
59
  try:
51
- init_env("欢迎使用 Jarvis 安全套件!", None)
60
+ init_env()
52
61
  except Exception:
53
62
  # 环境初始化失败不应阻塞CLI基础功能,继续后续流程
54
63
  pass
@@ -62,6 +71,9 @@ def agent(
62
71
  path,
63
72
  llm_group=llm_group,
64
73
  cluster_limit=cluster_limit,
74
+ enable_verification=enable_verification,
75
+ force_save_memory=force_save_memory,
76
+ output_file=output,
65
77
  )
66
78
  except Exception as e:
67
79
  try:
@@ -76,7 +88,18 @@ def agent(
76
88
  except Exception:
77
89
  pass
78
90
  result = direct_scan(path)
79
- text = format_markdown_report(result)
91
+ # 根据输出文件后缀选择格式
92
+ if output and output.lower().endswith('.csv'):
93
+ # 使用 report.py 中的函数来格式化 CSV
94
+ report_json = aggregate_issues(
95
+ result.get("issues", []),
96
+ scanned_root=result.get("summary", {}).get("scanned_root"),
97
+ scanned_files=result.get("summary", {}).get("scanned_files"),
98
+ )
99
+ text = format_csv_report(report_json)
100
+ else:
101
+ # 使用 workflow.py 中的 format_markdown_report(与 direct_scan 返回结构匹配)
102
+ text = format_markdown_report_workflow(result)
80
103
 
81
104
  if output:
82
105
  try: