Initial release: airsdb
This commit is contained in:
46
.claude-plugin/plugin.json
Normal file
46
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "airsdb",
|
||||||
|
"version": "0.1.1",
|
||||||
|
"description": "Cppcheck static-analysis workflow for local and remote C/C++ code quality, security-relevant findings, exhaustive branch-analysis detail, AirDbg or AirDo handoff reports, and staticanalysis.md maintenance.",
|
||||||
|
"author": {
|
||||||
|
"name": "14816",
|
||||||
|
"email": "noreply@example.com",
|
||||||
|
"url": "https://airlongdian.fun/plugins/airsdb"
|
||||||
|
},
|
||||||
|
"homepage": "https://airlongdian.fun/plugins/airsdb",
|
||||||
|
"repository": "http://git.airlongdian.fun/admin/airsdb.git",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"airsdb",
|
||||||
|
"cppcheck",
|
||||||
|
"static-analysis",
|
||||||
|
"c",
|
||||||
|
"cpp",
|
||||||
|
"code-quality",
|
||||||
|
"security",
|
||||||
|
"airdbg",
|
||||||
|
"airdo"
|
||||||
|
],
|
||||||
|
"skills": "./skills/",
|
||||||
|
"interface": {
|
||||||
|
"displayName": "AirSDB",
|
||||||
|
"shortDescription": "Cppcheck static analysis with exhaustive local and remote reports",
|
||||||
|
"longDescription": "AirSDB helps Codex run cppcheck static analysis for C or C++ projects, detect or auto-configure cppcheck on first startup, run local scans using compile_commands.json when available, default local and remote scans to --check-level=exhaustive for maximum branch-analysis detail, run remote scans over SSH with remote cppcheck auto-configuration when possible, write detailed XML or JSON artifacts under AirPlan/state/airsdb/reports, maintain concise AirPlan/docs/staticanalysis.md reports for AI context, and hand static-analysis evidence to AirDbg and AirDo.",
|
||||||
|
"developerName": "14816",
|
||||||
|
"category": "Productivity",
|
||||||
|
"capabilities": [
|
||||||
|
"Interactive",
|
||||||
|
"Write"
|
||||||
|
],
|
||||||
|
"websiteURL": "https://airlongdian.fun/plugins/airsdb",
|
||||||
|
"privacyPolicyURL": "https://openai.com/policies/row-privacy-policy/",
|
||||||
|
"termsOfServiceURL": "https://openai.com/policies/row-terms-of-use/",
|
||||||
|
"defaultPrompt": [
|
||||||
|
"Use AirSDB to run cppcheck and capture detailed static-analysis evidence for a local C or C++ codebase.",
|
||||||
|
"Use AirSDB to prepare or run a remote cppcheck scan over SSH and bring the findings back into the project context.",
|
||||||
|
"Use AirSDB to provide static-analysis evidence to AirDbg or AirDo before we change C or C++ code."
|
||||||
|
],
|
||||||
|
"brandColor": "#0F766E",
|
||||||
|
"screenshots": []
|
||||||
|
}
|
||||||
|
}
|
||||||
77
commands/airsdb.md
Normal file
77
commands/airsdb.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
description: Enter, inspect, run, or remote-run AirSDB cppcheck static analysis with exhaustive branch checking
|
||||||
|
argument-hint: [enter|setup|status|scan|command|remote-setup|remote-status|remote-command|remote-scan|exit]
|
||||||
|
allowed-tools: [Read, Glob, Grep, Bash, Write, Edit]
|
||||||
|
---
|
||||||
|
|
||||||
|
# /airsdb
|
||||||
|
|
||||||
|
控制当前工作区的 AirSDB 静态分析模式。
|
||||||
|
|
||||||
|
用户传入参数:`$ARGUMENTS`
|
||||||
|
|
||||||
|
- `enter` 或空参数:进入 AirSDB 模式,初始化 `AirPlan/docs/staticanalysis.md` 并检测 cppcheck。
|
||||||
|
- `setup`:检测/自动配置本机 cppcheck。
|
||||||
|
- `status`:检查 AirSDB 状态和本机 cppcheck,不自动安装。
|
||||||
|
- `command`:生成本机 cppcheck 命令,不实际执行。
|
||||||
|
- `scan`:运行本机 cppcheck,生成 XML/JSON 并更新 `AirPlan/docs/staticanalysis.md`。
|
||||||
|
- `remote-setup`:检查远程 SSH 和远端 cppcheck;缺失时自动尝试配置。
|
||||||
|
- `remote-status`:只检查远程配置和工具状态,不自动安装。
|
||||||
|
- `remote-command`:生成远程 cppcheck 命令,不实际执行。
|
||||||
|
- `remote-scan`:通过 SSH 在远端运行 cppcheck,拉回报告并更新 `AirPlan/docs/staticanalysis.md`。
|
||||||
|
- `exit`:退出 AirSDB 模式。
|
||||||
|
|
||||||
|
## 执行步骤
|
||||||
|
|
||||||
|
1. 解析 `$ARGUMENTS`,默认动作为 `enter`。
|
||||||
|
2. `enter` / `setup` / `status` / `exit` 时运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python "$HOME/plugins/airsdb/scripts/airsdb_mode.py" --mode <enter|setup|status|exit> --project .
|
||||||
|
```
|
||||||
|
|
||||||
|
如果 `python` 不存在,尝试 `py`、`python3` 或用户提供的 Python 绝对路径。
|
||||||
|
|
||||||
|
3. 本机 `command` / `scan` 时运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python "$HOME/plugins/airsdb/scripts/airsdb_cppcheck.py" --project . --action <command|scan>
|
||||||
|
```
|
||||||
|
|
||||||
|
本机 scan 会优先使用 `compile_commands.json`,否则扫描当前项目并排除常见噪声目录;默认带 `--check-level=exhaustive` 以提供尽可能详细的分支分析信息。结果写入 `AirPlan/state/airsdb/reports/`,摘要写入 `AirPlan/docs/staticanalysis.md`。
|
||||||
|
|
||||||
|
4. 如果 `$ARGUMENTS` 包含 `remote`、`ssh`、`远程`,或用户说明目标代码在远程设备、测试机、服务器、VM、容器宿主机或 SSH 主机上,优先运行远程 helper:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python "$HOME/plugins/airsdb/scripts/airsdb_remote_device.py" --project . --action <setup|status|command|scan>
|
||||||
|
```
|
||||||
|
|
||||||
|
首次缺少远程配置时,helper 会生成 `AirPlan/state/airsdb/remote-device.env.example` 并提示设置 `AIRSDB_REMOTE_SSH_TARGET` 和 `AIRSDB_REMOTE_PROJECT`。有 SSH 目标后,helper 会探测远端 cppcheck;缺失时自动尝试安装,只使用非交互式 `sudo -n`,无法自动配置时停止并提示用户。远程 scan 同样默认带 `--check-level=exhaustive`。
|
||||||
|
|
||||||
|
5. 每次 scan 后必须检查并维护:
|
||||||
|
- `AirPlan/docs/staticanalysis.md`
|
||||||
|
- `AirPlan/state/airsdb/reports/<timestamp>-cppcheck.xml`
|
||||||
|
- `AirPlan/state/airsdb/reports/<timestamp>-cppcheck.json`
|
||||||
|
- 必要时更新 `AGENTS.md`、ADR、C4 module
|
||||||
|
|
||||||
|
## 输出文案
|
||||||
|
|
||||||
|
进入模式:
|
||||||
|
|
||||||
|
```text
|
||||||
|
AirSDB 模式已开启:已初始化或检查 `AirPlan/docs/staticanalysis.md`,并检测 cppcheck。可运行本机 scan,也可在远程目标上使用 remote device helper 自动检测/配置 cppcheck;本机和远程 scan 默认使用 `--check-level=exhaustive`。
|
||||||
|
```
|
||||||
|
|
||||||
|
扫描完成:
|
||||||
|
|
||||||
|
```text
|
||||||
|
AirSDB 静态分析完成:XML/JSON 报告已写入 `AirPlan/state/airsdb/reports/`,简短 AI 上下文已追加到 `AirPlan/docs/staticanalysis.md`,可交给 AirDbg 或 AirDo 使用。
|
||||||
|
```
|
||||||
|
|
||||||
|
状态检查:
|
||||||
|
|
||||||
|
```text
|
||||||
|
AirSDB 状态:<enabled|disabled>
|
||||||
|
cppcheck:<ok|blocked>
|
||||||
|
关键文件:逐项列出 ok/missing。
|
||||||
|
```
|
||||||
320
scripts/airsdb_cppcheck.py
Normal file
320
scripts/airsdb_cppcheck.py
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run local cppcheck scans and maintain AirSDB reports."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from collections import Counter
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from airsdb_mode import ensure_files, read_env, setup_cppcheck, stamp
|
||||||
|
|
||||||
|
DEFAULT_ENABLE = "warning,style,performance,portability,information"
|
||||||
|
DEFAULT_CHECK_LEVEL = "exhaustive"
|
||||||
|
DEFAULT_EXCLUDES = [
|
||||||
|
".git",
|
||||||
|
".airsdb",
|
||||||
|
"node_modules",
|
||||||
|
"build",
|
||||||
|
"cmake-build-debug",
|
||||||
|
"cmake-build-release",
|
||||||
|
"dist",
|
||||||
|
"out",
|
||||||
|
"vendor",
|
||||||
|
"third_party",
|
||||||
|
"external",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def iso() -> str:
|
||||||
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def rel(project: Path, path: str) -> str:
|
||||||
|
if not path:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return str(Path(path).resolve().relative_to(project))
|
||||||
|
except Exception:
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def find_project_file(project: Path, configured: str = "") -> Optional[Path]:
|
||||||
|
candidates = []
|
||||||
|
if configured:
|
||||||
|
candidates.append(Path(configured))
|
||||||
|
candidates.extend(
|
||||||
|
[
|
||||||
|
project / "compile_commands.json",
|
||||||
|
project / "build" / "compile_commands.json",
|
||||||
|
project / "cmake-build-debug" / "compile_commands.json",
|
||||||
|
project / "cmake-build-release" / "compile_commands.json",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
for candidate in candidates:
|
||||||
|
p = candidate if candidate.is_absolute() else project / candidate
|
||||||
|
if p.exists():
|
||||||
|
return p.resolve()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def build_args(
|
||||||
|
project: Path,
|
||||||
|
tool: str,
|
||||||
|
project_file: str = "",
|
||||||
|
target: str = "",
|
||||||
|
enable: str = DEFAULT_ENABLE,
|
||||||
|
check_level: str = DEFAULT_CHECK_LEVEL,
|
||||||
|
jobs: int = 1,
|
||||||
|
std: str = "",
|
||||||
|
extra: Optional[List[str]] = None,
|
||||||
|
) -> List[str]:
|
||||||
|
build_dir = project / "AirPlan" / "state" / "airsdb" / "cppcheck-build"
|
||||||
|
build_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
args = [
|
||||||
|
tool,
|
||||||
|
f"--enable={enable}",
|
||||||
|
f"--check-level={check_level}",
|
||||||
|
"--inconclusive",
|
||||||
|
"--inline-suppr",
|
||||||
|
"--quiet",
|
||||||
|
"--xml",
|
||||||
|
"--xml-version=2",
|
||||||
|
f"--cppcheck-build-dir={build_dir}",
|
||||||
|
]
|
||||||
|
if jobs > 1:
|
||||||
|
args.append(f"-j{jobs}")
|
||||||
|
if std:
|
||||||
|
args.append(f"--std={std}")
|
||||||
|
for item in extra or []:
|
||||||
|
if item:
|
||||||
|
args.extend(shlex.split(item, posix=(os.name != "nt")))
|
||||||
|
|
||||||
|
pf = find_project_file(project, project_file)
|
||||||
|
if pf:
|
||||||
|
args.append(f"--project={pf}")
|
||||||
|
else:
|
||||||
|
for ignored in DEFAULT_EXCLUDES:
|
||||||
|
args.append(f"-i{ignored}")
|
||||||
|
args.append(target or ".")
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def command_text(args: List[str]) -> str:
|
||||||
|
if os.name == "nt":
|
||||||
|
return subprocess.list2cmdline(args)
|
||||||
|
return shlex.join(args)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_xml(text: str) -> str:
|
||||||
|
start = text.find("<?xml")
|
||||||
|
if start >= 0:
|
||||||
|
return text[start:]
|
||||||
|
start = text.find("<results")
|
||||||
|
if start >= 0:
|
||||||
|
return text[start:]
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def parse_cppcheck_xml(xml_text: str, project: Path) -> Dict[str, Any]:
|
||||||
|
xml_text = extract_xml(xml_text)
|
||||||
|
findings: List[Dict[str, Any]] = []
|
||||||
|
counts: Counter[str] = Counter()
|
||||||
|
if not xml_text.strip():
|
||||||
|
return {"counts": {}, "findings": [], "parseError": "empty_xml"}
|
||||||
|
try:
|
||||||
|
root = ET.fromstring(xml_text)
|
||||||
|
except ET.ParseError as exc:
|
||||||
|
return {"counts": {}, "findings": [], "parseError": str(exc)}
|
||||||
|
for error in root.findall(".//error"):
|
||||||
|
severity = error.attrib.get("severity", "unknown")
|
||||||
|
counts[severity] += 1
|
||||||
|
locations = []
|
||||||
|
for loc in error.findall("location"):
|
||||||
|
locations.append(
|
||||||
|
{
|
||||||
|
"file": rel(project, loc.attrib.get("file", "")),
|
||||||
|
"line": loc.attrib.get("line", ""),
|
||||||
|
"info": loc.attrib.get("info", ""),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
findings.append(
|
||||||
|
{
|
||||||
|
"severity": severity,
|
||||||
|
"id": error.attrib.get("id", ""),
|
||||||
|
"msg": error.attrib.get("msg", ""),
|
||||||
|
"cwe": error.attrib.get("cwe", ""),
|
||||||
|
"locations": locations,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {"counts": dict(counts), "findings": findings, "parseError": ""}
|
||||||
|
|
||||||
|
|
||||||
|
def top_findings(findings: List[Dict[str, Any]], limit: int = 12) -> List[str]:
|
||||||
|
rank = {"error": 0, "warning": 1, "performance": 2, "portability": 3, "style": 4, "information": 5}
|
||||||
|
|
||||||
|
def key(item: Dict[str, Any]) -> tuple[int, str]:
|
||||||
|
return (rank.get(item.get("severity", ""), 9), item.get("id", ""))
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for item in sorted(findings, key=key)[:limit]:
|
||||||
|
loc = item.get("locations") or [{}]
|
||||||
|
first = loc[0]
|
||||||
|
where = first.get("file", "")
|
||||||
|
if first.get("line"):
|
||||||
|
where += f":{first['line']}"
|
||||||
|
cwe = f" CWE-{item['cwe']}" if item.get("cwe") else ""
|
||||||
|
lines.append(f"[{item.get('severity')}:{item.get('id')}{cwe}] {where} {item.get('msg')}".strip())
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def append_staticanalysis(project: Path, data: Dict[str, Any]) -> None:
|
||||||
|
ensure_files(project)
|
||||||
|
path = project / "AirPlan" / "docs" / "staticanalysis.md"
|
||||||
|
counts = data.get("counts") or {}
|
||||||
|
counts_text = ", ".join(f"{k}={v}" for k, v in sorted(counts.items())) or "none"
|
||||||
|
tops = data.get("topFindings") or []
|
||||||
|
top_text = "\n".join(f" - {line}" for line in tops) if tops else " - none"
|
||||||
|
result = "failed" if data.get("returnCode", 0) not in [0, None] else ("findings" if data.get("findingCount", 0) else "ok")
|
||||||
|
entry = (
|
||||||
|
f"\n### {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: AirSDB cppcheck\n\n"
|
||||||
|
f"- Target: {data.get('target', 'local')}\n"
|
||||||
|
f"- Tool: `{data.get('tool', '')}` {data.get('version', '')}\n"
|
||||||
|
f"- Command: `{data.get('command', '')}`\n"
|
||||||
|
f"- Result: {result}; returnCode={data.get('returnCode', '')}\n"
|
||||||
|
f"- Counts: {counts_text}\n"
|
||||||
|
f"- Reports: `{data.get('xmlReport', '')}`, `{data.get('jsonReport', '')}`\n"
|
||||||
|
f"- Top findings:\n{top_text}\n"
|
||||||
|
f"- AirDbg/AirDo handoff: {data.get('handoff', 'Review top findings before code-level repair or todo validation.')}\n"
|
||||||
|
f"- Residual risk: cppcheck is static analysis and may miss runtime, integration, configuration, or dependency issues.\n"
|
||||||
|
)
|
||||||
|
path.open("a", encoding="utf-8", newline="\n").write(entry)
|
||||||
|
|
||||||
|
|
||||||
|
def write_reports(project: Path, data: Dict[str, Any], xml_text: str, prefix: str = "cppcheck") -> Dict[str, Any]:
|
||||||
|
reports = project / "AirPlan" / "state" / "airsdb" / "reports"
|
||||||
|
reports.mkdir(parents=True, exist_ok=True)
|
||||||
|
ts = stamp()
|
||||||
|
xml_path = reports / f"{ts}-{prefix}.xml"
|
||||||
|
json_path = reports / f"{ts}-{prefix}.json"
|
||||||
|
xml_path.write_text(xml_text, encoding="utf-8", newline="\n")
|
||||||
|
data["xmlReport"] = str(xml_path)
|
||||||
|
data["jsonReport"] = str(json_path)
|
||||||
|
json_path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", newline="\n")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def run_scan(args: argparse.Namespace) -> int:
|
||||||
|
project = Path(args.project).expanduser().resolve()
|
||||||
|
ensure_files(project)
|
||||||
|
setup = setup_cppcheck(project, auto=not args.no_auto_configure)
|
||||||
|
if setup.get("status") != "ok":
|
||||||
|
print(f"airsdb_scan_status=blocked")
|
||||||
|
for key, value in setup.items():
|
||||||
|
print(f"{key}={value}")
|
||||||
|
return 2
|
||||||
|
tool = setup["tool"]
|
||||||
|
env = read_env(project / "AirPlan" / "state" / "airsdb" / "tool.env")
|
||||||
|
extra = list(args.extra or [])
|
||||||
|
if env.get("AIRSDB_CPPCHECK_OPTIONS"):
|
||||||
|
extra.append(env["AIRSDB_CPPCHECK_OPTIONS"])
|
||||||
|
cmd = build_args(project, tool, args.project_file, args.target, args.enable, args.check_level, args.jobs, args.std, extra)
|
||||||
|
if args.action == "command":
|
||||||
|
print("airsdb_command_status=ok")
|
||||||
|
print(f"command={command_text(cmd)}")
|
||||||
|
return 0
|
||||||
|
try:
|
||||||
|
run = subprocess.run(cmd, cwd=project, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=args.timeout)
|
||||||
|
except subprocess.TimeoutExpired as exc:
|
||||||
|
data = {
|
||||||
|
"target": "local",
|
||||||
|
"status": "timeout",
|
||||||
|
"tool": tool,
|
||||||
|
"version": setup.get("version", ""),
|
||||||
|
"checkLevel": args.check_level,
|
||||||
|
"command": command_text(cmd),
|
||||||
|
"returnCode": "timeout",
|
||||||
|
"stdout": exc.stdout or "",
|
||||||
|
"stderr": exc.stderr or "",
|
||||||
|
"capturedAt": iso(),
|
||||||
|
"counts": {},
|
||||||
|
"findings": [],
|
||||||
|
"findingCount": 0,
|
||||||
|
"topFindings": [],
|
||||||
|
}
|
||||||
|
data = write_reports(project, data, str(exc.stderr or ""), "cppcheck-timeout")
|
||||||
|
append_staticanalysis(project, data)
|
||||||
|
print("airsdb_scan_status=timeout")
|
||||||
|
print(f"jsonReport={data['jsonReport']}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
xml_text = extract_xml(run.stderr or "")
|
||||||
|
parsed = parse_cppcheck_xml(xml_text, project)
|
||||||
|
findings = parsed.get("findings", [])
|
||||||
|
data = {
|
||||||
|
"target": "local",
|
||||||
|
"status": "ok" if run.returncode == 0 else "failed",
|
||||||
|
"tool": tool,
|
||||||
|
"version": setup.get("version", ""),
|
||||||
|
"checkLevel": args.check_level,
|
||||||
|
"command": command_text(cmd),
|
||||||
|
"returnCode": run.returncode,
|
||||||
|
"stdout": run.stdout.strip(),
|
||||||
|
"stderrPreview": (run.stderr or "")[:2000],
|
||||||
|
"capturedAt": iso(),
|
||||||
|
"counts": parsed.get("counts", {}),
|
||||||
|
"findings": findings,
|
||||||
|
"findingCount": len(findings),
|
||||||
|
"topFindings": top_findings(findings, args.limit),
|
||||||
|
"parseError": parsed.get("parseError", ""),
|
||||||
|
}
|
||||||
|
data = write_reports(project, data, xml_text, "cppcheck")
|
||||||
|
append_staticanalysis(project, data)
|
||||||
|
print(f"airsdb_scan_status={data['status']}")
|
||||||
|
print(f"findingCount={data['findingCount']}")
|
||||||
|
print(f"xmlReport={data['xmlReport']}")
|
||||||
|
print(f"jsonReport={data['jsonReport']}")
|
||||||
|
return 0 if run.returncode == 0 else 1
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="Run local AirSDB cppcheck analysis.")
|
||||||
|
parser.add_argument("--project", default=".")
|
||||||
|
parser.add_argument("--action", choices=["setup", "status", "command", "scan"], default="scan")
|
||||||
|
parser.add_argument("--project-file", default="")
|
||||||
|
parser.add_argument("--target", default="")
|
||||||
|
parser.add_argument("--enable", default=DEFAULT_ENABLE)
|
||||||
|
parser.add_argument("--check-level", default=DEFAULT_CHECK_LEVEL)
|
||||||
|
parser.add_argument("--std", default="")
|
||||||
|
parser.add_argument("--jobs", type=int, default=1)
|
||||||
|
parser.add_argument("--timeout", type=int, default=900)
|
||||||
|
parser.add_argument("--limit", type=int, default=12)
|
||||||
|
parser.add_argument("--extra", action="append", default=[])
|
||||||
|
parser.add_argument("--no-auto-configure", action="store_true")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = parse_args()
|
||||||
|
project = Path(args.project).expanduser().resolve()
|
||||||
|
if args.action in ["setup", "status"]:
|
||||||
|
ensure_files(project)
|
||||||
|
setup = setup_cppcheck(project, auto=(args.action == "setup" and not args.no_auto_configure))
|
||||||
|
print(f"airsdb_cppcheck_status={setup.get('status', 'unknown')}")
|
||||||
|
for key, value in setup.items():
|
||||||
|
if key != "status":
|
||||||
|
print(f"{key}={value}")
|
||||||
|
raise SystemExit(0 if setup.get("status") == "ok" else 2)
|
||||||
|
raise SystemExit(run_scan(args))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
318
scripts/airsdb_mode.py
Normal file
318
scripts/airsdb_mode.py
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Bootstrap AirSDB static-analysis context and cppcheck tooling."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Optional, Tuple
|
||||||
|
|
||||||
|
MARKER_BEGIN = "<!-- AIRSDB:BEGIN -->"
|
||||||
|
MARKER_END = "<!-- AIRSDB:END -->"
|
||||||
|
|
||||||
|
|
||||||
|
def now_iso() -> str:
|
||||||
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def stamp() -> str:
|
||||||
|
return datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||||
|
|
||||||
|
|
||||||
|
def read_env(path: Path) -> Dict[str, str]:
|
||||||
|
vals: Dict[str, str] = {}
|
||||||
|
if path.exists():
|
||||||
|
for raw in path.read_text(encoding="utf-8-sig").splitlines():
|
||||||
|
line = raw.strip()
|
||||||
|
if line and not line.startswith("#") and "=" in line:
|
||||||
|
key, value = line.split("=", 1)
|
||||||
|
vals[key.strip()] = value.strip().strip('"').strip("'")
|
||||||
|
for key in ["AIRSDB_CPPCHECK", "AIRSDB_CPPCHECK_OPTIONS"]:
|
||||||
|
if os.environ.get(key):
|
||||||
|
vals[key] = os.environ[key]
|
||||||
|
return vals
|
||||||
|
|
||||||
|
|
||||||
|
def write_env(path: Path, updates: Dict[str, str]) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
old = path.read_text(encoding="utf-8") if path.exists() else ""
|
||||||
|
keys = set(updates)
|
||||||
|
lines = []
|
||||||
|
for raw in old.splitlines():
|
||||||
|
key = raw.split("=", 1)[0].strip() if "=" in raw and not raw.strip().startswith("#") else None
|
||||||
|
if key not in keys:
|
||||||
|
lines.append(raw)
|
||||||
|
if lines and lines[-1].strip():
|
||||||
|
lines.append("")
|
||||||
|
for key, value in updates.items():
|
||||||
|
if value:
|
||||||
|
lines.append(f"{key}={value}")
|
||||||
|
path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8", newline="\n")
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_files(project: Path) -> None:
|
||||||
|
air = project / "AirPlan" / "state" / "airsdb"
|
||||||
|
air.mkdir(parents=True, exist_ok=True)
|
||||||
|
example = air / "tool.env.example"
|
||||||
|
if not example.exists():
|
||||||
|
example.write_text(
|
||||||
|
"# AirSDB local cppcheck configuration\n"
|
||||||
|
"AIRSDB_CPPCHECK=cppcheck\n"
|
||||||
|
"AIRSDB_CPPCHECK_OPTIONS=\n"
|
||||||
|
"# AirSDB defaults to --check-level=exhaustive for maximum branch analysis detail.\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
newline="\n",
|
||||||
|
)
|
||||||
|
gitignore = air / ".gitignore"
|
||||||
|
old = gitignore.read_text(encoding="utf-8") if gitignore.exists() else ""
|
||||||
|
for item in ["tool.env", "remote-device.env", "reports/", "cppcheck-build/"]:
|
||||||
|
if item not in old.splitlines():
|
||||||
|
old = (old.rstrip() + f"\n{item}\n").lstrip()
|
||||||
|
gitignore.write_text(old, encoding="utf-8", newline="\n")
|
||||||
|
|
||||||
|
static = project / "AirPlan" / "docs" / "staticanalysis.md"
|
||||||
|
if not static.exists():
|
||||||
|
static.write_text(
|
||||||
|
"# Static Analysis\n\n"
|
||||||
|
"Short AI-context reports for AirSDB cppcheck runs. Keep entries concise.\n\n"
|
||||||
|
"## Entry Template\n\n"
|
||||||
|
"### YYYY-MM-DD HH:MM:SS: AirSDB cppcheck\n\n"
|
||||||
|
"- Target: TODO\n"
|
||||||
|
"- Tool: TODO\n"
|
||||||
|
"- Command: TODO\n"
|
||||||
|
"- Result: TODO\n"
|
||||||
|
"- Counts: TODO\n"
|
||||||
|
"- Reports: TODO\n"
|
||||||
|
"- Top findings: TODO\n"
|
||||||
|
"- AirDbg/AirDo handoff: TODO\n"
|
||||||
|
"- Residual risk: TODO\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
newline="\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def airsdb_agents_block() -> str:
|
||||||
|
return f"""{MARKER_BEGIN}
|
||||||
|
## AirSDB Static Analysis Workflow
|
||||||
|
|
||||||
|
1. Use AirSDB for `/airsdb` sessions that run cppcheck static analysis for C/C++ code quality, security-relevant defects, and debugging evidence.
|
||||||
|
2. On first entry, detect cppcheck; if missing, auto-configure it with the best available package manager or stop with install instructions.
|
||||||
|
3. Maintain `AirPlan/docs/staticanalysis.md` as the short AI-context report for AirDbg and AirDo. Keep detailed XML/JSON artifacts under `AirPlan/state/airsdb/reports/`.
|
||||||
|
4. Prefer `compile_commands.json` when available; otherwise scan the narrowest useful source tree with common generated/vendor directories excluded.
|
||||||
|
5. Default local and remote cppcheck scans to `--check-level=exhaustive` so AirSDB provides the most detailed branch-analysis evidence it can.
|
||||||
|
6. For local analysis, use `airsdb_cppcheck.py`; for remote targets over SSH, use `airsdb_remote_device.py` before local analysis.
|
||||||
|
7. When remote cppcheck is missing, the remote helper may auto-configure it with non-interactive package-manager commands; if sudo/password or unsupported OS blocks it, stop and record the blocker.
|
||||||
|
8. Record command, target, report paths, counts, top findings, AirDbg/AirDo handoff, and residual risk in `AirPlan/docs/staticanalysis.md`.
|
||||||
|
9. Update ADR/C4 only when static analysis tooling becomes a durable project boundary or changes implementation decisions.
|
||||||
|
{MARKER_END}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def upsert_agents_md(path: Path) -> str:
|
||||||
|
block = airsdb_agents_block().rstrip() + "\n"
|
||||||
|
if path.exists():
|
||||||
|
original = path.read_text(encoding="utf-8")
|
||||||
|
existed = True
|
||||||
|
else:
|
||||||
|
original = "# AGENTS.md\n\n"
|
||||||
|
existed = False
|
||||||
|
begin = original.find(MARKER_BEGIN)
|
||||||
|
end = original.find(MARKER_END)
|
||||||
|
if begin >= 0 and end > begin:
|
||||||
|
end += len(MARKER_END)
|
||||||
|
updated = original[:begin].rstrip() + "\n\n" + block + original[end:].lstrip()
|
||||||
|
status = "updated"
|
||||||
|
else:
|
||||||
|
updated = original.rstrip() + "\n\n" + block
|
||||||
|
status = "updated" if existed else "created"
|
||||||
|
path.write_text(updated, encoding="utf-8", newline="\n")
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
def run_args(args: list[str], timeout: int = 600) -> subprocess.CompletedProcess[str]:
|
||||||
|
return subprocess.run(args, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def run_shell(cmd: str, timeout: int = 600) -> subprocess.CompletedProcess[str]:
|
||||||
|
return subprocess.run(cmd, shell=True, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def cppcheck_version(tool: str) -> str:
|
||||||
|
try:
|
||||||
|
result = run_args([tool, "--version"], timeout=20)
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
text = (result.stdout or result.stderr).strip()
|
||||||
|
return text.splitlines()[0] if text else ""
|
||||||
|
|
||||||
|
|
||||||
|
def detect_cppcheck(project: Path) -> Tuple[Optional[str], str]:
|
||||||
|
env = read_env(project / "AirPlan" / "state" / "airsdb" / "tool.env")
|
||||||
|
candidates = []
|
||||||
|
configured = env.get("AIRSDB_CPPCHECK", "").strip()
|
||||||
|
if configured:
|
||||||
|
candidates.append(configured)
|
||||||
|
candidates.append("cppcheck")
|
||||||
|
if platform.system().lower() == "windows":
|
||||||
|
candidates.extend(
|
||||||
|
[
|
||||||
|
r"C:\Program Files\Cppcheck\cppcheck.exe",
|
||||||
|
r"C:\Program Files (x86)\Cppcheck\cppcheck.exe",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
for candidate in candidates:
|
||||||
|
path = shutil.which(candidate) or candidate
|
||||||
|
if Path(path).exists() or shutil.which(path):
|
||||||
|
version = cppcheck_version(path)
|
||||||
|
if version:
|
||||||
|
return path, version
|
||||||
|
return None, ""
|
||||||
|
|
||||||
|
|
||||||
|
def install_cppcheck(project: Path) -> Dict[str, str]:
|
||||||
|
system = platform.system().lower()
|
||||||
|
attempts = []
|
||||||
|
if system == "windows":
|
||||||
|
package_commands = [
|
||||||
|
["winget", "install", "--id", "Cppcheck.Cppcheck", "-e", "--accept-source-agreements", "--accept-package-agreements", "--silent"],
|
||||||
|
["choco", "install", "cppcheck", "-y"],
|
||||||
|
["scoop", "install", "cppcheck"],
|
||||||
|
]
|
||||||
|
for cmd in package_commands:
|
||||||
|
exe = shutil.which(cmd[0])
|
||||||
|
if not exe:
|
||||||
|
continue
|
||||||
|
run = run_args([exe] + cmd[1:], timeout=1200)
|
||||||
|
attempts.append({"command": " ".join(cmd), "returncode": str(run.returncode)})
|
||||||
|
tool, version = detect_cppcheck(project)
|
||||||
|
if tool:
|
||||||
|
write_env(project / "AirPlan" / "state" / "airsdb" / "tool.env", {"AIRSDB_CPPCHECK": tool})
|
||||||
|
return {"status": "ok", "tool": tool, "version": version, "installer": cmd[0], "attempts": json.dumps(attempts)}
|
||||||
|
return {
|
||||||
|
"status": "blocked",
|
||||||
|
"reason": "cppcheck_not_found",
|
||||||
|
"hint": "Install Cppcheck from https://cppcheck.sourceforge.io/ or configure AIRSDB_CPPCHECK.",
|
||||||
|
"attempts": json.dumps(attempts),
|
||||||
|
}
|
||||||
|
|
||||||
|
commands = [
|
||||||
|
"if command -v apt-get >/dev/null 2>&1; then sudo -n apt-get update && sudo -n apt-get install -y cppcheck; "
|
||||||
|
"elif command -v dnf >/dev/null 2>&1; then sudo -n dnf install -y cppcheck; "
|
||||||
|
"elif command -v yum >/dev/null 2>&1; then sudo -n yum install -y cppcheck; "
|
||||||
|
"elif command -v apk >/dev/null 2>&1; then sudo -n apk add cppcheck; "
|
||||||
|
"elif command -v pacman >/dev/null 2>&1; then sudo -n pacman -Sy --noconfirm cppcheck; "
|
||||||
|
"elif command -v brew >/dev/null 2>&1; then brew install cppcheck; "
|
||||||
|
"elif command -v port >/dev/null 2>&1; then sudo -n port install cppcheck; "
|
||||||
|
"else exit 42; fi"
|
||||||
|
]
|
||||||
|
run = run_shell(commands[0], timeout=1200)
|
||||||
|
attempts.append({"command": "system-package-manager", "returncode": str(run.returncode)})
|
||||||
|
tool, version = detect_cppcheck(project)
|
||||||
|
if tool:
|
||||||
|
write_env(project / "AirPlan" / "state" / "airsdb" / "tool.env", {"AIRSDB_CPPCHECK": tool})
|
||||||
|
return {"status": "ok", "tool": tool, "version": version, "installer": "system-package-manager", "attempts": json.dumps(attempts)}
|
||||||
|
return {
|
||||||
|
"status": "blocked",
|
||||||
|
"reason": "cppcheck_not_found",
|
||||||
|
"hint": "Install cppcheck with the OS package manager or configure AIRSDB_CPPCHECK.",
|
||||||
|
"attempts": json.dumps(attempts),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def setup_cppcheck(project: Path, auto: bool = True) -> Dict[str, str]:
|
||||||
|
ensure_files(project)
|
||||||
|
tool, version = detect_cppcheck(project)
|
||||||
|
if tool:
|
||||||
|
write_env(project / "AirPlan" / "state" / "airsdb" / "tool.env", {"AIRSDB_CPPCHECK": tool})
|
||||||
|
return {"status": "ok", "tool": tool, "version": version, "autoConfigure": "skipped"}
|
||||||
|
if not auto:
|
||||||
|
return {"status": "blocked", "reason": "cppcheck_not_found", "example": str(project / "AirPlan" / "state" / "airsdb" / "tool.env.example")}
|
||||||
|
return install_cppcheck(project)
|
||||||
|
|
||||||
|
|
||||||
|
def artifact_map(project: Path) -> Dict[str, Path]:
|
||||||
|
return {
|
||||||
|
"AGENTS.md": project / "AirPlan" / "AGENTS.md",
|
||||||
|
"staticanalysis": project / "AirPlan" / "docs" / "staticanalysis.md",
|
||||||
|
"state": project / "AirPlan" / "state" / "airsdb" / "state.json",
|
||||||
|
"tool_env": project / "AirPlan" / "state" / "airsdb" / "tool.env",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def write_state(path: Path, enabled: bool, project: Path, tool_info: Dict[str, str]) -> None:
|
||||||
|
artifacts = artifact_map(project)
|
||||||
|
payload = {
|
||||||
|
"enabled": enabled,
|
||||||
|
"updatedAt": now_iso(),
|
||||||
|
"projectRoot": str(project),
|
||||||
|
"cppcheck": tool_info,
|
||||||
|
"artifactHealth": {name: p.exists() for name, p in artifacts.items() if name != "state"},
|
||||||
|
}
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", newline="\n")
|
||||||
|
|
||||||
|
|
||||||
|
def enter_mode(project: Path, auto: bool = True) -> Tuple[str, Dict[str, str]]:
|
||||||
|
ensure_files(project)
|
||||||
|
results = {"AGENTS.md": upsert_agents_md(project / "AirPlan" / "AGENTS.md"), "staticanalysis": "exists"}
|
||||||
|
tool_info = setup_cppcheck(project, auto=auto)
|
||||||
|
for key, value in tool_info.items():
|
||||||
|
results[f"cppcheck_{key}"] = value
|
||||||
|
write_state(project / "AirPlan" / "state" / "airsdb" / "state.json", True, project, tool_info)
|
||||||
|
return "enabled", results
|
||||||
|
|
||||||
|
|
||||||
|
def exit_mode(project: Path) -> Tuple[str, Dict[str, str]]:
|
||||||
|
ensure_files(project)
|
||||||
|
tool_info = setup_cppcheck(project, auto=False)
|
||||||
|
write_state(project / "AirPlan" / "state" / "airsdb" / "state.json", False, project, tool_info)
|
||||||
|
return "disabled", {}
|
||||||
|
|
||||||
|
|
||||||
|
def status_mode(project: Path) -> Tuple[str, Dict[str, str]]:
|
||||||
|
ensure_files(project)
|
||||||
|
state = project / "AirPlan" / "state" / "airsdb" / "state.json"
|
||||||
|
enabled = False
|
||||||
|
if state.exists():
|
||||||
|
try:
|
||||||
|
enabled = bool(json.loads(state.read_text(encoding="utf-8")).get("enabled"))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
enabled = False
|
||||||
|
tool_info = setup_cppcheck(project, auto=False)
|
||||||
|
results = {name: ("ok" if path.exists() else "missing") for name, path in artifact_map(project).items() if name != "state"}
|
||||||
|
for key, value in tool_info.items():
|
||||||
|
results[f"cppcheck_{key}"] = value
|
||||||
|
return ("enabled" if enabled else "disabled"), results
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="Manage AirSDB static-analysis artifacts.")
|
||||||
|
parser.add_argument("--mode", choices=["enter", "setup", "exit", "status"], default="enter")
|
||||||
|
parser.add_argument("--project", default=".")
|
||||||
|
parser.add_argument("--no-auto-configure", action="store_true")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = parse_args()
|
||||||
|
project = Path(args.project).expanduser().resolve()
|
||||||
|
if args.mode in ["enter", "setup"]:
|
||||||
|
mode_state, result = enter_mode(project, auto=not args.no_auto_configure)
|
||||||
|
elif args.mode == "exit":
|
||||||
|
mode_state, result = exit_mode(project)
|
||||||
|
else:
|
||||||
|
mode_state, result = status_mode(project)
|
||||||
|
print(f"airsdb_mode={mode_state}")
|
||||||
|
print(f"project_root={project}")
|
||||||
|
for key, value in result.items():
|
||||||
|
print(f"{key}={value}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
314
scripts/airsdb_remote_device.py
Normal file
314
scripts/airsdb_remote_device.py
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run AirSDB cppcheck on a remote SSH target."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
from airsdb_cppcheck import append_staticanalysis, parse_cppcheck_xml, top_findings, write_reports
|
||||||
|
from airsdb_mode import ensure_files, stamp
|
||||||
|
|
||||||
|
DEFAULT_ENABLE = "warning,style,performance,portability,information"
|
||||||
|
DEFAULT_CHECK_LEVEL = "exhaustive"
|
||||||
|
|
||||||
|
|
||||||
|
def iso() -> str:
|
||||||
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def q(value: str) -> str:
|
||||||
|
return shlex.quote(str(value))
|
||||||
|
|
||||||
|
|
||||||
|
def split(value: str) -> List[str]:
|
||||||
|
return shlex.split(value, posix=True) if value.strip() else []
|
||||||
|
|
||||||
|
|
||||||
|
def read_env(path: Path) -> Dict[str, str]:
|
||||||
|
vals: Dict[str, str] = {}
|
||||||
|
if path.exists():
|
||||||
|
for raw in path.read_text(encoding="utf-8-sig").splitlines():
|
||||||
|
line = raw.strip()
|
||||||
|
if line and not line.startswith("#") and "=" in line:
|
||||||
|
key, value = line.split("=", 1)
|
||||||
|
vals[key.strip()] = value.strip().strip('"').strip("'")
|
||||||
|
for key in [
|
||||||
|
"AIRSDB_REMOTE_SSH_TARGET",
|
||||||
|
"AIRSDB_REMOTE_SSH_PORT",
|
||||||
|
"AIRSDB_REMOTE_SSH_OPTIONS",
|
||||||
|
"AIRSDB_REMOTE_WORKDIR",
|
||||||
|
"AIRSDB_REMOTE_PROJECT",
|
||||||
|
"AIRSDB_REMOTE_CPPCHECK",
|
||||||
|
]:
|
||||||
|
if os.environ.get(key):
|
||||||
|
vals[key] = os.environ[key]
|
||||||
|
vals.setdefault("AIRSDB_REMOTE_SSH_PORT", "22")
|
||||||
|
vals.setdefault("AIRSDB_REMOTE_CPPCHECK", "auto")
|
||||||
|
return vals
|
||||||
|
|
||||||
|
|
||||||
|
def write_env(path: Path, updates: Dict[str, str]) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
old = path.read_text(encoding="utf-8") if path.exists() else ""
|
||||||
|
keys = set(updates)
|
||||||
|
lines = []
|
||||||
|
for raw in old.splitlines():
|
||||||
|
key = raw.split("=", 1)[0].strip() if "=" in raw and not raw.strip().startswith("#") else None
|
||||||
|
if key not in keys:
|
||||||
|
lines.append(raw)
|
||||||
|
if lines and lines[-1].strip():
|
||||||
|
lines.append("")
|
||||||
|
for key, value in updates.items():
|
||||||
|
if value:
|
||||||
|
lines.append(f"{key}={value}")
|
||||||
|
path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8", newline="\n")
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_remote_files(project: Path) -> None:
|
||||||
|
ensure_files(project)
|
||||||
|
air = project / "AirPlan" / "state" / "airsdb"
|
||||||
|
example = air / "remote-device.env.example"
|
||||||
|
if not example.exists():
|
||||||
|
example.write_text(
|
||||||
|
"# AirSDB remote cppcheck configuration\n"
|
||||||
|
"AIRSDB_REMOTE_SSH_TARGET=user@host\n"
|
||||||
|
"AIRSDB_REMOTE_SSH_PORT=22\n"
|
||||||
|
"AIRSDB_REMOTE_SSH_OPTIONS=\n"
|
||||||
|
"AIRSDB_REMOTE_WORKDIR=\n"
|
||||||
|
"AIRSDB_REMOTE_PROJECT=/path/to/remote/project\n"
|
||||||
|
"AIRSDB_REMOTE_CPPCHECK=auto\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
newline="\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ssh_args(env: Dict[str, str], cmd: str) -> List[str]:
|
||||||
|
target = env.get("AIRSDB_REMOTE_SSH_TARGET", "")
|
||||||
|
if not target:
|
||||||
|
raise SystemExit("AIRSDB_REMOTE_SSH_TARGET is required")
|
||||||
|
args = [shutil.which("ssh") or "ssh"]
|
||||||
|
port = env.get("AIRSDB_REMOTE_SSH_PORT", "22")
|
||||||
|
if port:
|
||||||
|
args += ["-p", port]
|
||||||
|
args += split(env.get("AIRSDB_REMOTE_SSH_OPTIONS", ""))
|
||||||
|
return args + [target, cmd]
|
||||||
|
|
||||||
|
|
||||||
|
def run(env: Dict[str, str], cmd: str, timeout: int = 60) -> subprocess.CompletedProcess[str]:
|
||||||
|
return subprocess.run(ssh_args(env, cmd), capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def remote_home(env: Dict[str, str]) -> str:
|
||||||
|
result = run(env, 'printf %s "$HOME"', 15)
|
||||||
|
return result.stdout.strip() if result.returncode == 0 else ""
|
||||||
|
|
||||||
|
|
||||||
|
def workdir(env: Dict[str, str]) -> str:
|
||||||
|
if env.get("AIRSDB_REMOTE_WORKDIR"):
|
||||||
|
return env["AIRSDB_REMOTE_WORKDIR"]
|
||||||
|
home = remote_home(env)
|
||||||
|
return (home.rstrip("/") + "/.airsdb") if home else ".airsdb"
|
||||||
|
|
||||||
|
|
||||||
|
def detect_tool(env: Dict[str, str]) -> str:
|
||||||
|
tool = env.get("AIRSDB_REMOTE_CPPCHECK", "auto").strip()
|
||||||
|
if tool and tool != "auto":
|
||||||
|
result = run(env, f"command -v {q(tool)} >/dev/null 2>&1 && command -v {q(tool)} || test -x {q(tool)} && printf %s {q(tool)}", 20)
|
||||||
|
return result.stdout.strip().splitlines()[-1] if result.returncode == 0 and result.stdout.strip() else ""
|
||||||
|
result = run(env, 'command -v cppcheck >/dev/null 2>&1 && command -v cppcheck', 20)
|
||||||
|
return result.stdout.strip().splitlines()[-1] if result.returncode == 0 and result.stdout.strip() else ""
|
||||||
|
|
||||||
|
|
||||||
|
def remote_version(env: Dict[str, str], tool: str) -> str:
|
||||||
|
result = run(env, f"{q(tool)} --version", 20)
|
||||||
|
text = (result.stdout or result.stderr).strip()
|
||||||
|
return text.splitlines()[0] if result.returncode == 0 and text else ""
|
||||||
|
|
||||||
|
|
||||||
|
def install_tool(env: Dict[str, str]) -> subprocess.CompletedProcess[str]:
|
||||||
|
script = """set -e
|
||||||
|
if command -v cppcheck >/dev/null 2>&1; then exit 0; fi
|
||||||
|
if [ "$(id -u 2>/dev/null || echo 1)" = "0" ]; then SUDO=""; else SUDO="sudo -n"; fi
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then $SUDO apt-get update && $SUDO apt-get install -y cppcheck;
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then $SUDO dnf install -y cppcheck;
|
||||||
|
elif command -v yum >/dev/null 2>&1; then $SUDO yum install -y cppcheck;
|
||||||
|
elif command -v apk >/dev/null 2>&1; then $SUDO apk add cppcheck;
|
||||||
|
elif command -v pacman >/dev/null 2>&1; then $SUDO pacman -Sy --noconfirm cppcheck;
|
||||||
|
elif command -v brew >/dev/null 2>&1; then brew install cppcheck;
|
||||||
|
elif command -v port >/dev/null 2>&1; then $SUDO port install cppcheck;
|
||||||
|
else exit 42; fi"""
|
||||||
|
return run(env, script, 1200)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(project: Path, env: Dict[str, str], auto: bool = True) -> Dict[str, str]:
|
||||||
|
if not env.get("AIRSDB_REMOTE_SSH_TARGET"):
|
||||||
|
return {"status": "blocked", "reason": "missing_remote_target", "example": str(project / "AirPlan" / "state" / "airsdb" / "remote-device.env.example")}
|
||||||
|
if not shutil.which("ssh"):
|
||||||
|
return {"status": "blocked", "reason": "ssh_not_found"}
|
||||||
|
probe = run(env, "printf ok", 20)
|
||||||
|
if probe.returncode:
|
||||||
|
return {"status": "blocked", "reason": "ssh_probe_failed", "stderr": probe.stderr.strip()}
|
||||||
|
wd = workdir(env)
|
||||||
|
mk = run(env, f"mkdir -p {q(wd)} {q(wd.rstrip('/') + '/reports')} {q(wd.rstrip('/') + '/cppcheck-build')}", 30)
|
||||||
|
if mk.returncode:
|
||||||
|
return {"status": "blocked", "reason": "remote_workdir_failed", "stderr": mk.stderr.strip()}
|
||||||
|
tool = detect_tool(env)
|
||||||
|
install = "skipped"
|
||||||
|
if not tool and auto:
|
||||||
|
inst = install_tool(env)
|
||||||
|
install = "ok" if inst.returncode == 0 else f"failed:{inst.returncode}"
|
||||||
|
tool = detect_tool(env)
|
||||||
|
updates = {"AIRSDB_REMOTE_WORKDIR": wd}
|
||||||
|
if tool:
|
||||||
|
updates["AIRSDB_REMOTE_CPPCHECK"] = tool
|
||||||
|
write_env(project / "AirPlan" / "state" / "airsdb" / "remote-device.env", updates)
|
||||||
|
version = remote_version(env, tool) if tool else ""
|
||||||
|
return {
|
||||||
|
"status": "ok" if tool else "blocked",
|
||||||
|
"target": env.get("AIRSDB_REMOTE_SSH_TARGET", ""),
|
||||||
|
"workdir": wd,
|
||||||
|
"tool": tool,
|
||||||
|
"version": version,
|
||||||
|
"autoConfigure": install,
|
||||||
|
"hint": "" if tool else "Install cppcheck on the remote target or set AIRSDB_REMOTE_CPPCHECK.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def scan_cmd(
|
||||||
|
env: Dict[str, str],
|
||||||
|
tool: str,
|
||||||
|
remote_project: str,
|
||||||
|
remote_xml: str,
|
||||||
|
enable: str,
|
||||||
|
check_level: str,
|
||||||
|
std: str,
|
||||||
|
extra: List[str],
|
||||||
|
) -> str:
|
||||||
|
build_dir = workdir(env).rstrip("/") + "/cppcheck-build"
|
||||||
|
report_dir = remote_xml.rsplit("/", 1)[0] if "/" in remote_xml else "."
|
||||||
|
options = [
|
||||||
|
q(tool),
|
||||||
|
f"--enable={q(enable)}",
|
||||||
|
f"--check-level={q(check_level)}",
|
||||||
|
"--inconclusive",
|
||||||
|
"--inline-suppr",
|
||||||
|
"--quiet",
|
||||||
|
"--xml",
|
||||||
|
"--xml-version=2",
|
||||||
|
f"--cppcheck-build-dir={q(build_dir)}",
|
||||||
|
]
|
||||||
|
if std:
|
||||||
|
options.append(f"--std={q(std)}")
|
||||||
|
for item in extra:
|
||||||
|
if item:
|
||||||
|
options.append(item)
|
||||||
|
return (
|
||||||
|
f"cd {q(remote_project)} && mkdir -p {q(report_dir)} {q(build_dir)} && "
|
||||||
|
"if [ -f compile_commands.json ]; then AIRSDB_PROJECT_ARG='--project=compile_commands.json'; "
|
||||||
|
"elif [ -f build/compile_commands.json ]; then AIRSDB_PROJECT_ARG='--project=build/compile_commands.json'; "
|
||||||
|
"elif [ -f cmake-build-debug/compile_commands.json ]; then AIRSDB_PROJECT_ARG='--project=cmake-build-debug/compile_commands.json'; "
|
||||||
|
"elif [ -f cmake-build-release/compile_commands.json ]; then AIRSDB_PROJECT_ARG='--project=cmake-build-release/compile_commands.json'; "
|
||||||
|
"else AIRSDB_PROJECT_ARG='.'; fi; "
|
||||||
|
+ " ".join(options)
|
||||||
|
+ f" $AIRSDB_PROJECT_ARG 2> {q(remote_xml)}; AIRSDB_RC=$?; "
|
||||||
|
'printf "airsdb_cppcheck_rc=%s\\n" "$AIRSDB_RC"; '
|
||||||
|
'printf "airsdb_project_arg=%s\\n" "$AIRSDB_PROJECT_ARG"; '
|
||||||
|
"exit 0"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def emit(prefix: str, data: Dict[str, object]) -> None:
|
||||||
|
print(f"{prefix}_status={data.get('status', 'unknown')}")
|
||||||
|
for key, value in data.items():
|
||||||
|
if key != "status":
|
||||||
|
print(f"{key}={json.dumps(value, ensure_ascii=False) if isinstance(value, (dict, list)) else value}")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_stdout_value(stdout: str, key: str) -> str:
|
||||||
|
prefix = f"{key}="
|
||||||
|
for line in stdout.splitlines():
|
||||||
|
if line.startswith(prefix):
|
||||||
|
return line[len(prefix) :].strip()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Run AirSDB cppcheck on a remote SSH target.")
|
||||||
|
parser.add_argument("--project", default=".")
|
||||||
|
parser.add_argument("--action", choices=["setup", "status", "command", "scan"], default="setup")
|
||||||
|
parser.add_argument("--remote-project", default="")
|
||||||
|
parser.add_argument("--enable", default=DEFAULT_ENABLE)
|
||||||
|
parser.add_argument("--check-level", default=DEFAULT_CHECK_LEVEL)
|
||||||
|
parser.add_argument("--std", default="")
|
||||||
|
parser.add_argument("--timeout", type=int, default=900)
|
||||||
|
parser.add_argument("--limit", type=int, default=12)
|
||||||
|
parser.add_argument("--extra", action="append", default=[])
|
||||||
|
parser.add_argument("--no-auto-configure", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
project = Path(args.project).expanduser().resolve()
|
||||||
|
ensure_remote_files(project)
|
||||||
|
env = read_env(project / "AirPlan" / "state" / "airsdb" / "remote-device.env")
|
||||||
|
info = setup(project, env, auto=(args.action != "status" and not args.no_auto_configure))
|
||||||
|
if args.action in ["setup", "status"] or info.get("status") != "ok":
|
||||||
|
emit("airsdb_remote", info)
|
||||||
|
raise SystemExit(0 if info.get("status") == "ok" else 2)
|
||||||
|
|
||||||
|
remote_project = args.remote_project or env.get("AIRSDB_REMOTE_PROJECT", "")
|
||||||
|
if not remote_project:
|
||||||
|
emit("airsdb_remote", {"status": "blocked", "reason": "missing_remote_project", "example": str(project / "AirPlan" / "state" / "airsdb" / "remote-device.env.example")})
|
||||||
|
raise SystemExit(2)
|
||||||
|
|
||||||
|
remote_xml = info["workdir"].rstrip("/") + "/reports/" + stamp() + "-airsdb-remote-cppcheck.xml"
|
||||||
|
cmd = scan_cmd(env, str(info["tool"]), remote_project, remote_xml, args.enable, args.check_level, args.std, args.extra)
|
||||||
|
full_command = shlex.join(ssh_args(env, cmd))
|
||||||
|
if args.action == "command":
|
||||||
|
emit("airsdb_remote", {"status": "ok", "target": info["target"], "remoteProject": remote_project, "remoteXml": remote_xml, "command": full_command})
|
||||||
|
return
|
||||||
|
|
||||||
|
run_result = run(env, cmd, args.timeout)
|
||||||
|
rc_text = parse_stdout_value(run_result.stdout, "airsdb_cppcheck_rc")
|
||||||
|
cppcheck_rc = int(rc_text) if rc_text.isdigit() else run_result.returncode
|
||||||
|
b64 = run(env, f"base64 < {q(remote_xml)}", 120)
|
||||||
|
xml_text = ""
|
||||||
|
if b64.returncode == 0 and b64.stdout.strip():
|
||||||
|
xml_text = base64.b64decode("".join(b64.stdout.split())).decode("utf-8", errors="replace")
|
||||||
|
parsed = parse_cppcheck_xml(xml_text, project)
|
||||||
|
findings = parsed.get("findings", [])
|
||||||
|
data = {
|
||||||
|
"target": f"remote:{info['target']}",
|
||||||
|
"remoteProject": remote_project,
|
||||||
|
"status": "ok" if run_result.returncode == 0 else "failed",
|
||||||
|
"tool": info.get("tool", ""),
|
||||||
|
"version": info.get("version", ""),
|
||||||
|
"checkLevel": args.check_level,
|
||||||
|
"command": full_command,
|
||||||
|
"returnCode": cppcheck_rc,
|
||||||
|
"sshReturnCode": run_result.returncode,
|
||||||
|
"stdout": run_result.stdout.strip(),
|
||||||
|
"stderr": run_result.stderr.strip(),
|
||||||
|
"remoteXml": remote_xml,
|
||||||
|
"capturedAt": iso(),
|
||||||
|
"counts": parsed.get("counts", {}),
|
||||||
|
"findings": findings,
|
||||||
|
"findingCount": len(findings),
|
||||||
|
"topFindings": top_findings(findings, args.limit),
|
||||||
|
"parseError": parsed.get("parseError", ""),
|
||||||
|
}
|
||||||
|
data = write_reports(project, data, xml_text, "remote-cppcheck")
|
||||||
|
append_staticanalysis(project, data)
|
||||||
|
emit("airsdb_remote", data)
|
||||||
|
raise SystemExit(0 if run_result.returncode == 0 else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
166
skills/airsdb/SKILL.md
Normal file
166
skills/airsdb/SKILL.md
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
---
|
||||||
|
name: airsdb
|
||||||
|
description: Cppcheck static-analysis workflow for C/C++ projects. Use when the user invokes /airsdb, asks to run static analysis, evaluate code quality or security with cppcheck, generate a short AI-context static-analysis report for AirDbg or AirDo, diagnose issues that need static analysis, maintain AirPlan/docs/staticanalysis.md, or run local/remote cppcheck over SSH. On first startup detect cppcheck and auto-install or auto-configure it when missing; default local and remote scans to `--check-level=exhaustive` for maximum branch-analysis detail; and call the AirSDB remote device helper when remote cppcheck is needed.
|
||||||
|
---
|
||||||
|
|
||||||
|
# AirSDB
|
||||||
|
|
||||||
|
## 核心约束
|
||||||
|
|
||||||
|
- 全程使用中文与用户交流,命令、路径、工具名、告警 id、CWE 保持原文。
|
||||||
|
- `/airsdb` 专用于 C/C++ 静态分析、代码质量/安全性初筛、cppcheck 证据收集,以及给 AirDbg/AirDo 提供简短 AI 上下文报告。
|
||||||
|
- 第一次进入必须检测 `cppcheck`。本机缺失时自动尝试用包管理器安装或配置;无法自动安装时停止并提示官方下载页或 `AIRSDB_CPPCHECK`。
|
||||||
|
- 必须创建或维护 `AirPlan/docs/staticanalysis.md`。它只写简短摘要,详细 XML/JSON 产物放在 `AirPlan/state/airsdb/reports/`。
|
||||||
|
- 本机分析使用 `../../scripts/airsdb_cppcheck.py`。
|
||||||
|
- 远程设备、测试机、VM、容器宿主机、服务器或 SSH 主机上的静态分析,先使用 `../../scripts/airsdb_remote_device.py`;远端缺少 `cppcheck` 时允许脚本自动配置,无法无密码 `sudo` 或无包管理器时停止并提示用户。
|
||||||
|
- 优先使用 `compile_commands.json`;没有时只扫描最窄可行目录,并排除 `.git`、`AirPlan/state/airsdb`、`build`、`node_modules`、`vendor`、`third_party` 等常见噪声目录。
|
||||||
|
- 默认使用 `--check-level=exhaustive`,尽可能提供详细分支分析信息,避免出现 `normalCheckLevelMaxBranches` 这类因分支分析深度受限造成的信息缺口;只有用户明确要求降级时才改。
|
||||||
|
- Cppcheck 是静态分析,不等同于编译、测试或安全审计;结论要写成“证据/线索”,不要夸大。
|
||||||
|
- 如果需要 cppcheck 安装和命令细节,读取 [references/cppcheck-notes.md](references/cppcheck-notes.md)。
|
||||||
|
|
||||||
|
## 启动与环境检测
|
||||||
|
|
||||||
|
进入 `/airsdb` 时运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python "$HOME/plugins/airsdb/scripts/airsdb_mode.py" --mode enter --project .
|
||||||
|
```
|
||||||
|
|
||||||
|
如果当前环境没有 `python`,尝试 `py`、`python3` 或用户提供的 Python 绝对路径。
|
||||||
|
|
||||||
|
脚本会:
|
||||||
|
|
||||||
|
- 初始化 `AirPlan/state/airsdb/`、`AirPlan/state/airsdb/tool.env.example`、`AirPlan/state/airsdb/.gitignore`。
|
||||||
|
- 创建或维护 `AirPlan/docs/staticanalysis.md`。
|
||||||
|
- 在 `AirPlan/AGENTS.md` 中维护 AirSDB 标记块。
|
||||||
|
- 检测 `AIRSDB_CPPCHECK`、`AirPlan/state/airsdb/tool.env`、PATH 和常见 Windows 安装路径。
|
||||||
|
- 找不到 `cppcheck` 时自动尝试安装:
|
||||||
|
- Windows:`winget`、`choco`、`scoop`
|
||||||
|
- Linux/macOS:`apt-get`、`dnf`、`yum`、`apk`、`pacman`、`brew`、`port`
|
||||||
|
|
||||||
|
`AirPlan/state/airsdb/tool.env` 是本机路径配置,由 `AirPlan/state/airsdb/.gitignore` 忽略,不应提交。
|
||||||
|
|
||||||
|
## 本机分析
|
||||||
|
|
||||||
|
检查或安装 cppcheck:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python "$HOME/plugins/airsdb/scripts/airsdb_cppcheck.py" --project . --action setup
|
||||||
|
```
|
||||||
|
|
||||||
|
只生成命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python "$HOME/plugins/airsdb/scripts/airsdb_cppcheck.py" --project . --action command
|
||||||
|
```
|
||||||
|
|
||||||
|
执行扫描:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python "$HOME/plugins/airsdb/scripts/airsdb_cppcheck.py" --project . --action scan --timeout 900
|
||||||
|
```
|
||||||
|
|
||||||
|
常用参数:
|
||||||
|
|
||||||
|
- `--project-file build/compile_commands.json`:指定编译数据库。
|
||||||
|
- `--target src`:没有编译数据库时限制扫描目录。
|
||||||
|
- `--enable warning,style,performance,portability,information`:默认检查集合。
|
||||||
|
- `--check-level exhaustive`:默认详细分支分析级别。
|
||||||
|
- `--std c++17`:指定 C/C++ 标准。
|
||||||
|
- `--extra "--suppress=missingIncludeSystem"`:追加 cppcheck 参数。
|
||||||
|
|
||||||
|
扫描后必须确认:
|
||||||
|
|
||||||
|
- `AirPlan/state/airsdb/reports/<timestamp>-cppcheck.xml`
|
||||||
|
- `AirPlan/state/airsdb/reports/<timestamp>-cppcheck.json`
|
||||||
|
- `AirPlan/docs/staticanalysis.md` 已追加简短报告
|
||||||
|
|
||||||
|
## 远程设备分析
|
||||||
|
|
||||||
|
当目标代码或复现场景在远程设备上时,不要先跑本机 cppcheck。先运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python "$HOME/plugins/airsdb/scripts/airsdb_remote_device.py" --project . --action setup
|
||||||
|
```
|
||||||
|
|
||||||
|
首次运行会生成 `AirPlan/state/airsdb/remote-device.env.example`。将连接信息写入 `AirPlan/state/airsdb/remote-device.env` 或当前环境变量:
|
||||||
|
|
||||||
|
- `AIRSDB_REMOTE_SSH_TARGET=user@host`
|
||||||
|
- `AIRSDB_REMOTE_SSH_PORT=22`
|
||||||
|
- `AIRSDB_REMOTE_SSH_OPTIONS=`
|
||||||
|
- `AIRSDB_REMOTE_WORKDIR=`
|
||||||
|
- `AIRSDB_REMOTE_PROJECT=/path/to/remote/project`
|
||||||
|
- `AIRSDB_REMOTE_CPPCHECK=auto`
|
||||||
|
|
||||||
|
远程 helper 行为:
|
||||||
|
|
||||||
|
- 检查本机 `ssh`、远程连通性、远程工作目录。
|
||||||
|
- 探测远端 `cppcheck`。
|
||||||
|
- 缺失时自动尝试用远端包管理器安装 `cppcheck`,只使用非交互式 `sudo -n`;需要密码、管理员确认或无支持包管理器时停止并提示用户。
|
||||||
|
- 在远端项目目录运行 cppcheck,把 XML 拉回本机 `AirPlan/state/airsdb/reports/` 并更新本机 `AirPlan/docs/staticanalysis.md`。
|
||||||
|
|
||||||
|
远程命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python "$HOME/plugins/airsdb/scripts/airsdb_remote_device.py" --project . --action command
|
||||||
|
python "$HOME/plugins/airsdb/scripts/airsdb_remote_device.py" --project . --action scan --timeout 900
|
||||||
|
```
|
||||||
|
|
||||||
|
## 与 AirDbg 协作
|
||||||
|
|
||||||
|
AirDbg 调试中遇到以下情况时调用 AirSDB:
|
||||||
|
|
||||||
|
- 需要用静态分析辅助定位崩溃、内存错误、未初始化变量、空指针、越界、危险转换、资源释放或 CWE 线索。
|
||||||
|
- 需要在修复前后比较 cppcheck 结果。
|
||||||
|
- 需要给根因分析提供短报告,而不是完整 XML 噪声。
|
||||||
|
|
||||||
|
AirSDB 给 AirDbg 的交接必须写入 `AirPlan/docs/staticanalysis.md`:
|
||||||
|
|
||||||
|
- 命令和目标
|
||||||
|
- XML/JSON 报告路径
|
||||||
|
- severity/id/CWE 计数
|
||||||
|
- Top findings
|
||||||
|
- 哪些 findings 与当前 bug 相关
|
||||||
|
- 剩余风险
|
||||||
|
|
||||||
|
## 与 AirDo 协作
|
||||||
|
|
||||||
|
AirDo 执行 `AirPlan/todo.md` 时可以调用 AirSDB 做验收或排障:
|
||||||
|
|
||||||
|
- todo 要求静态分析、质量检查、安全性初筛或 C/C++ 代码风险评估。
|
||||||
|
- 验证失败但需要 cppcheck 辅助定位。
|
||||||
|
- 远程设备上的实现需要远端 cppcheck 证据。
|
||||||
|
|
||||||
|
AirDo 仍然拥有 `AirPlan/todo.md` 进度。调用 AirSDB 后,把命令、报告路径、结论和剩余风险写回当前 todo 项。
|
||||||
|
|
||||||
|
## staticanalysis.md 维护
|
||||||
|
|
||||||
|
每次 AirSDB 扫描至少追加:
|
||||||
|
|
||||||
|
- Target:local 或 remote target
|
||||||
|
- Tool:cppcheck 路径和版本
|
||||||
|
- Command:实际命令
|
||||||
|
- Result:ok / findings / failed
|
||||||
|
- Counts:各 severity 数量
|
||||||
|
- Reports:XML/JSON 路径
|
||||||
|
- Top findings:最多 12 条,含 severity、id、CWE、文件行号、摘要
|
||||||
|
- AirDbg/AirDo handoff:当前任务如何使用这些结果
|
||||||
|
- Residual risk:静态分析未覆盖的风险
|
||||||
|
|
||||||
|
不要把完整 XML、长日志或大段 cppcheck 输出塞进 `staticanalysis.md`。
|
||||||
|
|
||||||
|
## AGENTS / ADR / C4
|
||||||
|
|
||||||
|
- 发现稳定可复用的 AirSDB 命令、远程设备配置、过滤策略、suppressions 或质量门槛时,更新 `AGENTS.md`。
|
||||||
|
- 如果静态分析成为长期测试/调试边界,或影响模块边界、质量策略、安全策略、CI 策略,更新 C4 module 并新增或修订 ADR。
|
||||||
|
- 如果只是一次临时扫描,只维护 `staticanalysis.md` 即可。
|
||||||
|
|
||||||
|
## 完成输出
|
||||||
|
|
||||||
|
本轮结束时用中文简洁汇报:
|
||||||
|
|
||||||
|
- 使用本机还是远程 cppcheck。
|
||||||
|
- cppcheck 是否可用,是否发生自动配置。
|
||||||
|
- 报告路径和 `staticanalysis.md` 是否更新。
|
||||||
|
- 发现数量和最重要的 3-5 条线索。
|
||||||
|
- 是否建议交给 AirDbg 修复,或交给 AirDo 写回 todo 验收。
|
||||||
3
skills/airsdb/agents/openai.yaml
Normal file
3
skills/airsdb/agents/openai.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
name: airsdb
|
||||||
|
short_description: Cppcheck static analysis with exhaustive local/remote reports
|
||||||
|
default_prompt: "使用 AirSDB 运行本机或远程 cppcheck 静态分析,默认启用 --check-level=exhaustive,生成简短 staticanalysis.md 报告并交给 AirDbg/AirDo 使用。"
|
||||||
27
skills/airsdb/references/cppcheck-notes.md
Normal file
27
skills/airsdb/references/cppcheck-notes.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Cppcheck Notes
|
||||||
|
|
||||||
|
Use this only when AirSDB needs cppcheck install or command details.
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
- Official open-source download page: https://cppcheck.sourceforge.io/
|
||||||
|
- Official repository package notes: https://github.com/danmar/cppcheck
|
||||||
|
- Official manual: https://cppcheck.sourceforge.io/manual.html
|
||||||
|
- User-provided Chinese guide: https://www.zeeklog.com/cppcheckzhong-ji-zhi-nan-cong-ling-kai-shi-zhang-wo-c-c-jing-tai-dai-ma-fen-xi
|
||||||
|
- User-provided download reference: http://cppcheck.net/#download
|
||||||
|
|
||||||
|
## Install Notes
|
||||||
|
|
||||||
|
- The official page lists current open-source releases and package-manager examples.
|
||||||
|
- Windows official installer is linked from the Cppcheck open-source page.
|
||||||
|
- Package managers can be convenient but may lag behind official releases.
|
||||||
|
- AirSDB auto-configures with package managers first because it must be non-interactive for agent workflows.
|
||||||
|
|
||||||
|
## Command Notes
|
||||||
|
|
||||||
|
- Prefer `cppcheck --project=compile_commands.json` when the project has a compilation database.
|
||||||
|
- Generate a CMake compilation database with `cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON .` when appropriate.
|
||||||
|
- Use `--xml --xml-version=2` for machine-readable reports.
|
||||||
|
- Use `--cppcheck-build-dir=<path>` for incremental analysis and better whole-program analysis.
|
||||||
|
- Use `-i<path>` to skip generated/vendor directories.
|
||||||
|
- Use suppressions instead of deleting warnings when a finding is a known false positive.
|
||||||
Reference in New Issue
Block a user