Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

### Fixed
- Improved DSH subscription cleanup and artifact discovery, and made system cron status failures explicit.
- Fixed `checkup` to recursively scan plugins referenced by DSH bundles, wait for all DSH scans before report generation, and include per-plugin results in JSON and HTML reports.

## [1.1.29] - 2026-08-26

Expand Down
8 changes: 5 additions & 3 deletions docs/dsh.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,11 @@ Scheduled self-check discovery includes `$DSH_HOME/skills` (default
`~/.dsh/skills`), `<current-project>/.dsh/skills`, every immediate
`$DSH_HOME/profiles/*/package.json`, each profile's declared direct and optional
dependencies under `node_modules`, and existing `cordis.patch.yml` or
`cordis.patch.yaml` files in the DSH home and profile directories. Dependency
discovery is deliberately non-recursive: undeclared transitive packages and
dependency names that could escape `node_modules` are excluded. Advisory-level
`cordis.patch.yaml` files in the DSH home and profile directories. When a direct
dependency is a DSH bundle, discovery follows package dependencies named by its
Cordis plugin rows recursively, including pnpm virtual-store layouts. Unrelated
transitive packages and dependency names that could escape `node_modules` stay
excluded. Advisory-level
`inspectPaths` and explicitly supplied self-check roots remain authoritative.

For local checkout testing, install the CLI from a packed tarball but keep the
Expand Down
15 changes: 14 additions & 1 deletion skills/agentguard/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -1058,7 +1058,7 @@ Run these checks in parallel where possible. These are **universal agent securit

For **every** discovered skill, **run `/agentguard scan <skill_path>`** using the scan subcommand logic (24 detection rules). Do NOT skip any skill regardless of how many are found. Record for each skill: name, risk_level, and exact findings list (rule, severity, file, line).

Also discover installed DSH plugins under the resolved DSH home: use the non-empty `DSH_HOME` value when set, otherwise use the current user's real home directory plus `.dsh` (do not pass a literal unexpanded `~` to path APIs). Inspect `${dshHome}/profiles/*`; read each immediate profile's `package.json`, collect only names declared in `dependencies` and `optionalDependencies`, and resolve their existing directories beneath that same profile's `node_modules/`. Do not recursively walk `node_modules` or scan transitive-only dependencies. Exclude only the exact `@goplus/agentguard` dependency coordinate declared by the profile; never trust an installed plugin's self-reported package name for this exclusion. For **every** remaining direct plugin, run `agentguard dsh-scan <plugin_path> --format json` and record its name, `riskLevel`, and exact findings list (`ruleId`, severity, file, line), normalized to the raw-facts schema below. A failed DSH plugin scan must not prevent the remaining checks from completing; record it in `dsh_plugins` with `risk_level: "high"` and one finding `{ "rule": "DSH_SCAN_FAILED", "severity": "HIGH", "file": "<plugin_path>", "line": 0 }`.
Also discover installed DSH plugins under the resolved DSH home: use the non-empty `DSH_HOME` value when set, otherwise use the current user's real home directory plus `.dsh` (do not pass a literal unexpanded `~` to path APIs). Inspect `${dshHome}/profiles/*`; read each immediate profile's `package.json`, collect only names declared in `dependencies` and `optionalDependencies`, and resolve their existing directories beneath that same profile's `node_modules/`. For a dependency that declares `dsh.bundle.patch`, parse its Cordis patch and recursively resolve only package dependencies actually named by plugin rows in that patch; repeat for referenced child bundles, with path deduplication, cycle detection, and a bounded depth. Do not enumerate or scan unrelated transitive dependencies. Exclude only the exact `@goplus/agentguard` dependency coordinate declared by the profile or referenced by a bundle; never trust an installed plugin's self-reported package name for this exclusion. For **every** discovered direct or bundle-referenced plugin, run `agentguard dsh-scan <plugin_path> --format json` and record its name, path, `riskLevel`, and exact findings list (`ruleId`, severity, file, line), normalized to the raw-facts schema below. A failed DSH plugin scan must not prevent the remaining checks from completing; record it in `dsh_plugins` with `risk_level: "high"` and one finding `{ "rule": "DSH_SCAN_FAILED", "severity": "HIGH", "file": "<plugin_path>", "line": 0 }`.
2. **[REQUIRED] Credential file permissions** (→ feeds Dimension 2: Credential Safety): Platform-aware check — behavior differs by OS:
- **macOS/Linux**: Run `stat -f '%Lp' <path> 2>/dev/null || stat -c '%a' <path> 2>/dev/null` on `~/.ssh/`, `~/.gnupg/`. **If the command returns empty output, the directory does not exist — record `exists: false`.**
- **Windows**: `stat` is not available. Use `icacls <path>` to check ACLs instead. If directory doesn't exist, record `exists: false`. If it exists, record whether the ACL grants access to `Everyone`, `Users`, or `Authenticated Users`.
Expand All @@ -1078,6 +1078,8 @@ Run these checks in parallel where possible. These are **universal agent securit
6. **[REQUIRED] Environment variable exposure** (→ feeds Dimension 3: Network & System): Run `env` and check for sensitive variable names (`PRIVATE_KEY`, `MNEMONIC`, `SECRET`, `PASSWORD`) — detect presence only, mask values. Record list of sensitive variable names found.
7. **[REQUIRED] Runtime protection check** (→ feeds Dimension 4: Runtime Protection): Check if security hooks exist in `~/.claude/settings.json`, `~/.openclaw/openclaw.json`, or `~/.hermes/config.yaml`. Check for audit logs at `~/.agentguard/audit.jsonl`. Check if installed skills have been previously scanned (audit log contains `scan` events). Record booleans: `hooks_installed`, `audit_log_exists`, `skills_ever_scanned`.

**Completion barrier:** Parallel collection is allowed, but Step 2 MUST NOT begin while any skill scan, DSH plugin scan, bundle-child discovery, or other required check is still running. Await every spawned/background task, collect every exit status and output, and only then assemble raw facts. Never start `checkup-score.js` or `checkup-report.js` from the same parallel batch as a scan.

### Step 2: Assemble Raw Facts JSON

After completing all 7 checks, assemble the raw facts into a structured JSON and write it to a temporary file (e.g. `/tmp/agentguard-raw-facts.json`):
Expand All @@ -1096,6 +1098,7 @@ After completing all 7 checks, assemble the raw facts into a structured JSON and
"dsh_plugins": [
{
"name": "<plugin-name>",
"path": "<plugin-path>",
"risk_level": "<low|medium|high|critical>",
"findings": [
{ "rule": "<RULE_ID>", "severity": "<CRITICAL|HIGH|MEDIUM|LOW>", "file": "<filename>", "line": <number> }
Expand Down Expand Up @@ -1194,6 +1197,16 @@ Assemble the final JSON by merging the scored output from Step 3 with the analys
},
"skills_scanned": <count of skills from Step 1>,
"dsh_plugins_scanned": <count of successfully scanned DSH plugins from Step 1>,
"dsh_plugins": [
{
"name": "<plugin-name>",
"path": "<plugin-path>",
"risk_level": "<low|medium|high|critical>",
"findings": [
{ "rule": "<RULE_ID>", "severity": "<CRITICAL|HIGH|MEDIUM|LOW>", "file": "<filename>", "line": <number> }
]
}
],
"protection_level": "<level>",
"analysis": "<the comprehensive AI-written security analysis report>",
"recommendations": [
Expand Down
46 changes: 42 additions & 4 deletions skills/agentguard/scripts/checkup-report.js
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,7 @@ function pixelLobster(grade, color) {
// ---------------------------------------------------------------------------

function generateReport(data) {
const { composite_score = 0, dimensions = {}, recommendations = [], skills_scanned = 0, dsh_plugins_scanned = 0, protection_level = 'unknown', timestamp } = data;
const { composite_score = 0, dimensions = {}, recommendations = [], skills_scanned = 0, dsh_plugins_scanned = 0, dsh_plugins = [], protection_level = 'unknown', timestamp } = data;
const tier = getTier(composite_score);
const ctaUrl = `https://www.agentguard.one?utm_source=checkup&utm_medium=cli&utm_campaign=health_report&score=${composite_score}`;
const ts = timestamp || new Date().toISOString();
Expand Down Expand Up @@ -725,6 +725,35 @@ function generateReport(data) {
findingsPages.push(h);
}

// ── DSH plugin scan results ──
const dshPluginPages = [];
const normalizedDshPlugins = Array.isArray(dsh_plugins) ? dsh_plugins : [];
const DSH_PLUGINS_PER_PAGE = 4;
for (let i = 0; i < normalizedDshPlugins.length; i += DSH_PLUGINS_PER_PAGE) {
const chunk = normalizedDshPlugins.slice(i, i + DSH_PLUGINS_PER_PAGE);
let h = `<div class="mb-6"><p class="text-xs font-label uppercase tracking-[0.2em] text-[#849588] mb-1" data-i18n="dsh_scan_results">DSH Plugin Scan Results</p><h1 class="text-3xl font-headline font-bold text-[#f5fff5] tracking-tight flex items-center gap-3"><span class="material-symbols-outlined text-[#98cbff]">extension</span><span data-i18n="dsh_plugins">DSH Plugins</span> <span class="text-[#849588] font-normal text-lg">(${normalizedDshPlugins.length})</span></h1></div>`;
h += chunk.map(plugin => {
const risk = String(plugin?.risk_level || 'unknown').toUpperCase();
const color = sevColor(risk);
const pluginFindings = Array.isArray(plugin?.findings) ? plugin.findings : [];
const findingsHtml = pluginFindings.length > 0
? `<div class="mt-3 space-y-1">${pluginFindings.map(finding => {
const location = `${finding?.file || '?'}:${finding?.line ?? '?'}`;
return `<div class="text-xs text-[#b9cbbd] font-mono"><span style="color:${sevColor(finding?.severity)}">${esc(finding?.rule || 'UNKNOWN')}</span> <span class="text-[#849588]">${esc(location)}</span></div>`;
}).join('')}</div>`
: '<div class="mt-3 text-xs text-[#849588]" data-i18n="no_plugin_findings">No findings.</div>';
return `<div class="bg-[#1c2026] border border-[#3a4a3f]/15 rounded-xl p-4 mb-3" style="border-left:3px solid ${color}">
<div class="flex items-center justify-between gap-3">
<span class="font-headline font-semibold text-[#dfe2eb]">${esc(plugin?.name || 'unknown')}</span>
<span class="px-2.5 py-0.5 rounded text-[10px] font-bold uppercase tracking-wide text-white" style="background:${color}">${esc(risk)}</span>
</div>
<div class="mt-1 text-[10px] text-[#849588] font-mono break-all">${esc(plugin?.path || '')}</div>
${findingsHtml}
</div>`;
}).join('');
dshPluginPages.push(h);
}

// ── Recommendations ──
// Auto-generate extra recommendations based on dimension scores
const autoRecs = [];
Expand Down Expand Up @@ -783,7 +812,7 @@ function generateReport(data) {
const healthLabel = composite_score >= 70 ? 'OPTIMAL' : composite_score >= 50 ? 'STABILIZING' : 'CRITICAL_ALERT';

// ── Total pages ──
const totalPages = 1 + findingsPages.length + 1;
const totalPages = 1 + findingsPages.length + dshPluginPages.length + 1;

const html = `<!DOCTYPE html>
<html class="dark" lang="en"><head>
Expand Down Expand Up @@ -922,6 +951,15 @@ body{background:#0a0e14;color:#dfe2eb;font-family:'Inter',sans-serif}
</div>
</div>`).join('')}

<!-- DSH plugin scan results -->
${dshPluginPages.map(content => `
<div class="w-full h-full shrink-0 px-4 sm:px-6 py-4 sm:py-5 overflow-hidden">
<div class="h-full rounded-xl p-4 sm:p-6 flex flex-col overflow-y-auto border border-[#3a4a3f]/10 relative" style="background:linear-gradient(145deg,#1c2026 0%,#151b22 100%)">
<div class="absolute top-0 right-0 w-48 h-48 rounded-full blur-[100px] opacity-[0.03] pointer-events-none" style="background:#98cbff"></div>
${content}
</div>
</div>`).join('')}

<!-- LAST PAGE: Security Analysis & Remediation -->
<div class="w-full h-full shrink-0 px-4 sm:px-6 py-4 sm:py-5 overflow-hidden">
<div class="h-full rounded-xl p-4 sm:p-6 flex flex-col overflow-y-auto border border-[#3a4a3f]/10 relative" style="background:linear-gradient(145deg,#1c2026 0%,#151d20 100%)">
Expand Down Expand Up @@ -1043,8 +1081,8 @@ body{background:#0a0e14;color:#dfe2eb;font-family:'Inter',sans-serif}

// ── i18n ──
const i18n={
en:{title:'AgentGuard Report',share:'Share',diag_metrics:'Diagnostic Metrics',sec_dims:'SECURITY DIMENSIONS',back:'Back',next:'Next',nav_overview:'Overview',nav_analysis:'Analysis',nav_report:'Report',vuln_stream:'Active Vulnerability Stream',findings:'Findings',sec_analysis:'Security Analysis',diag_report:'Diagnostic Report',action_items:'Action Items',cta_title:'Enhanced Skill Scanning',cta_desc:'Deeper code analysis, threat intelligence feeds & real-time protection.',cta_btn:'Upgrade Skill Scanning',artifacts_scanned:'Scanned artifacts',findings_label:'Findings',tier_label:'Tier',copy_report:'Copy Report',system_health:'System Health',dim_code_safety:'Skill & Code Safety',dim_credential_safety:'Credential & Secrets',dim_network_exposure:'Network & System',dim_runtime_protection:'Runtime Protection',dim_web3_safety:'Web3 Safety',no_threats_clean:'No active threats detected. Clinically sterile.',all_clear:'All Clear',no_threats_all:'No active threats detected across all dimensions.',share_report_title:'Share Report',generating_preview:'Generating preview...',copy_image:'Copy image to clipboard',share_img_hint:'📋 Clicking a platform copies the image — just paste when posting',no_recs:'No recommendations.',tier_badge:'TIER ${tier.grade} — ${tier.label}',status_label:'STATUS: ${healthLabel}',prot_mode:'${protection_level} mode',download_btn:'Download'},
zh:{title:'AgentGuard 诊断报告',share:'分享',diag_metrics:'诊断指标',sec_dims:'安全维度',back:'上一页',next:'下一页',nav_overview:'总览',nav_analysis:'威胁分析',nav_report:'诊断报告',vuln_stream:'活跃漏洞流',findings:'发现',sec_analysis:'安全分析',diag_report:'诊断报告',action_items:'修复建议',cta_title:'更强的 Skill 扫描',cta_desc:'更深度的代码分析、威胁情报推送、实时安全防护',cta_btn:'升级到更强的skill扫描',artifacts_scanned:'已扫描项目',findings_label:'发现',tier_label:'等级',copy_report:'复制报告',system_health:'系统健康',dim_code_safety:'技能与代码安全',dim_credential_safety:'凭证与密钥安全',dim_network_exposure:'网络与系统暴露',dim_runtime_protection:'运行时防护',dim_web3_safety:'Web3 安全',no_threats_clean:'未检测到活跃威胁,环境安全无虞。',all_clear:'全部通过',no_threats_all:'所有维度均未检测到活跃威胁。',share_report_title:'分享报告',generating_preview:'正在生成预览...',copy_image:'复制图片到剪贴板',share_img_hint:'📋 点击平台按钮会自动复制图片,去粘贴发出去就行',no_recs:'暂无修复建议。',tier_badge:'等级 ${tier.grade} — ${{S:'强壮',A:'健康',B:'疲惫',F:'危急'}[tier.grade]||tier.label}',status_label:'状态: ${{OPTIMAL:'最佳',STABILIZING:'恢复中',CRITICAL_ALERT:'危急警报'}[healthLabel]||healthLabel}',prot_mode:'${{strict:'严格',balanced:'均衡',permissive:'宽松'}[protection_level]||protection_level} 模式',download_btn:'下载'}
en:{title:'AgentGuard Report',share:'Share',diag_metrics:'Diagnostic Metrics',sec_dims:'SECURITY DIMENSIONS',back:'Back',next:'Next',nav_overview:'Overview',nav_analysis:'Analysis',nav_report:'Report',vuln_stream:'Active Vulnerability Stream',findings:'Findings',dsh_scan_results:'DSH Plugin Scan Results',dsh_plugins:'DSH Plugins',no_plugin_findings:'No findings.',sec_analysis:'Security Analysis',diag_report:'Diagnostic Report',action_items:'Action Items',cta_title:'Enhanced Skill Scanning',cta_desc:'Deeper code analysis, threat intelligence feeds & real-time protection.',cta_btn:'Upgrade Skill Scanning',artifacts_scanned:'Scanned artifacts',findings_label:'Findings',tier_label:'Tier',copy_report:'Copy Report',system_health:'System Health',dim_code_safety:'Skill & Code Safety',dim_credential_safety:'Credential & Secrets',dim_network_exposure:'Network & System',dim_runtime_protection:'Runtime Protection',dim_web3_safety:'Web3 Safety',no_threats_clean:'No active threats detected. Clinically sterile.',all_clear:'All Clear',no_threats_all:'No active threats detected across all dimensions.',share_report_title:'Share Report',generating_preview:'Generating preview...',copy_image:'Copy image to clipboard',share_img_hint:'📋 Clicking a platform copies the image — just paste when posting',no_recs:'No recommendations.',tier_badge:'TIER ${tier.grade} — ${tier.label}',status_label:'STATUS: ${healthLabel}',prot_mode:'${protection_level} mode',download_btn:'Download'},
zh:{title:'AgentGuard 诊断报告',share:'分享',diag_metrics:'诊断指标',sec_dims:'安全维度',back:'上一页',next:'下一页',nav_overview:'总览',nav_analysis:'威胁分析',nav_report:'诊断报告',vuln_stream:'活跃漏洞流',findings:'发现',dsh_scan_results:'DSH 插件扫描结果',dsh_plugins:'DSH 插件',no_plugin_findings:'未发现问题。',sec_analysis:'安全分析',diag_report:'诊断报告',action_items:'修复建议',cta_title:'更强的 Skill 扫描',cta_desc:'更深度的代码分析、威胁情报推送、实时安全防护',cta_btn:'升级到更强的skill扫描',artifacts_scanned:'已扫描项目',findings_label:'发现',tier_label:'等级',copy_report:'复制报告',system_health:'系统健康',dim_code_safety:'技能与代码安全',dim_credential_safety:'凭证与密钥安全',dim_network_exposure:'网络与系统暴露',dim_runtime_protection:'运行时防护',dim_web3_safety:'Web3 安全',no_threats_clean:'未检测到活跃威胁,环境安全无虞。',all_clear:'全部通过',no_threats_all:'所有维度均未检测到活跃威胁。',share_report_title:'分享报告',generating_preview:'正在生成预览...',copy_image:'复制图片到剪贴板',share_img_hint:'📋 点击平台按钮会自动复制图片,去粘贴发出去就行',no_recs:'暂无修复建议。',tier_badge:'等级 ${tier.grade} — ${{S:'强壮',A:'健康',B:'疲惫',F:'危急'}[tier.grade]||tier.label}',status_label:'状态: ${{OPTIMAL:'最佳',STABILIZING:'恢复中',CRITICAL_ALERT:'危急警报'}[healthLabel]||healthLabel}',prot_mode:'${{strict:'严格',balanced:'均衡',permissive:'宽松'}[protection_level]||protection_level} 模式',download_btn:'下载'}
};
const _qzh={S:['"你的 Agent 壮得像头牛!💪 没有什么能突破这双钳子!"','"天生猛男,这只龙虾在举铁 🏋️"','"铜墙铁壁!这安全性简直满分 🤌"','"诺克斯堡?不,是龙虾堡 🦞🔒"','"巅峰状态!你的 Agent 把威胁当早餐吃 💪"'],A:['"状态不错!再调整一下就无敌了。"','"快了——再努力一下这只龙虾就能练出腹肌!🦞"','"盾牌就位,钳子锋利,只差最后一点打磨 🛡️"','"你的 Agent 状态很好——微调一下就是 S 级!✨"','"健康又警觉,这只龙虾每天晨跑五公里 🏃"'],B:['"你的 Agent 需要锻炼一下……还有来杯咖啡 ☕"','"困困龙虾,有潜力就是需要鸡血 😴"','"快没油了——该给这只甲壳动物加加油!⛽"','"你的 Agent 在刷剧,没空巡逻 📺"','"这只龙虾跳过了腿日……胳膊日……每一天 🦞💤"'],F:['"危急状态!这个 Agent 需要紧急救治!🚨"','"红色警报!这只龙虾正在被抢救!🏥"','"SOS!你的 Agent 正在用摩斯密码发求救信号 📡"','"求救求救!这只甲壳动物快不行了!🆘"','"你 Agent 的免疫系统已退出群聊 💀"']};
const quotes_zh=Object.fromEntries(Object.entries(_qzh).map(([k,v])=>[k,v[Math.floor(Math.random()*v.length)]]));
Expand Down
Loading
Loading