Skill: jooneyp/secucheck (version 2.8.0)

--- name: secucheck description: Comprehensive security audit for OpenClaw. Scans 7 domains (runtime, channels, agents, cron, skills, sessions, network), supports 3 expertise levels, context-aware analysis, and visual dashboard. Read-only with localized reports. tags: [security, audit, hardening, runtime, dashboard, prompt-injection] --- # secucheck - OpenClaw Security Audit Comprehensive security audit skill for OpenClaw deployments. Analyzes configuration, permissions, exposure risks, and runtime environment with context-aware recommendations. --- ## Summary **secucheck** performs read-only security audits of your OpenClaw setup: - **7 audit domains**: Runtime, Channels, Agents, Cron Jobs, Skills, Sessions, Network - **3 expertise levels**: Beginner (analogies), Intermediate (technical), Expert (attack vectors) - **Context-aware**: Considers VPN, single-user, self-hosted scenarios - **Runtime checks**: Live system state (network exposure, containers, privileges) - **Dashboard**: Visual HTML report with security score - **Localized output**: Final report matches user's language **Never modifies configuration automatically.** All fixes require explicit user confirmation. --- ## Quick Start ### Installation ```bash clawhub install secucheck ``` ### Usage Ask your OpenClaw agent: - "security audit" - "secucheck" - "run security check" ### Expertise Levels When prompted, choose your level: 1. **Beginner** - Simple analogies, no jargon 2. **Intermediate** - Technical details, config examples 3. **Expert** - Attack vectors, edge cases, CVEs All levels run the same checks—only explanation depth varies. ### Dashboard ``` "show dashboard" / "visual report" ``` Opens an HTML report in your browser. --- ## Example Output ``` 🔒 Security Audit Results 🟡 Needs Attention | Severity | Count | |----------|-------| | 🔴 Critical | 0 | | 🟠 High | 0 | | 🟡 Medium | 2 | | 🟢 Low | 3 | ### 🟡 Agent "molty": exec + external content processing ... ``` --- ## Features - 🔍 **Comprehensive**: Channels, agents, cron, skills, sessions, network, runtime - 👤 **3 Expertise Levels**: Beginner / Intermediate / Expert - 🌏 **Localized**: Final report in user's language - 🎯 **Attack Scenarios**: Real-world exploitation paths - ⚡ **Runtime Checks**: VPN, containers, privileges, network exposure - 🎨 **Dashboard**: Visual HTML report with security score --- # Agent Instructions *Everything below is for the agent executing this skill.* --- ## When to Use Trigger this skill when: - User requests security checkup/audit - **Auto-trigger**: Installing skills, creating/modifying agents, adding/modifying cron jobs - Periodic review (recommended: weekly) ## Expertise Levels | Level | Identifier | Style | |-------|------------|-------| | Beginner | `1`, `beginner` | Analogies, simple explanations, no jargon | | Intermediate | `2`, `intermediate` | Technical details, config examples | | Expert | `3`, `expert` | Attack vectors, edge cases, CVE references | ## Execution Flow ### Step 1: Ask Level (before running anything) Present options in user's language. Example (English): ``` What level of technical detail do you prefer? 1. 🌱 Beginner - I'll explain simply with analogies 2. 💻 Intermediate - Technical details and config examples 3. 🔐 Expert - Include attack vectors and edge cases 📌 All levels run the same checks—only explanation depth varies. ``` **STOP HERE. Wait for user response.** ### Step 2: Run Audit ```bash bash ~/.openclaw/skills/secucheck/scripts/full_audit.sh ``` Returns JSON with findings categorized by severity. ### Step 3: Format Output Parse JSON output and format based on user's expertise level. **Final report must be in user's language.** #### Report Structure (Organize by Category) ``` 🔒 Security Audit Results 📊 Summary Table | Severity | Count | |----------|-------| | 🔴 Critical | X | | ... ⚡ Runtime - [findings related to RUNTIME category] 🤖 Agents - [findings related to AGENT category] 📁 Workspace - [findings related to WORKSPACE category] 🧩 Skills - [findings related to SKILL category] 📢 Channels - [findings related to CHANNEL category] 🌐 Network - [findings related to NETWORK category] ``` Group findings by their `category` field, not just severity. Within each category, show severity icon and explain. ### Step 4: Auto-Open Dashboard After text report, automatically generate and serve dashboard: ```bash bash ~/.openclaw/skills/secucheck/scripts/serve_dashboard.sh ``` The script returns JSON with `url` (LAN IP) and `local_url` (localhost). **Use the `url` field** (not localhost) when telling the user — they may access from another device. Example: ``` 📊 대시보드도 열었어요: http://192.168.1.200:8766/secucheck-report.html ``` If running in environment where browser can be opened, use browser tool to open it. ## Cross-Platform Support Scripts run on Linux, macOS, and WSL. Check the JSON output for platform info: ```json { "os": "linux", "os_variant": "ubuntu", "in_wsl": false, "in_dsm": false, "failed_checks": ["external_ip"] } ``` ### Platform Detection | Field | Values | |-------|--------| | `os` | `linux`, `macos`, `windows`, `unknown` | | `os_variant` | `ubuntu`, `arch`, `dsm`, `wsl`, version string | | `in_wsl` | `true` if Windows Subsystem for Linux | | `in_dsm` | `true` if Synology DSM | ### Handling Failed Checks If `failed_checks` array is non-empty, run fallback commands based on platform: #### Network Info Fallbacks | Platform | Command | |----------|---------| | Linux | `ip addr show` or `ifconfig` | | macOS | `ifconfig` | | WSL | `ip addr show` (or check Windows via `cmd.exe /c ipconfig`) | | Windows | PowerShell: `Get-NetIPAddress` | | DSM | `ifconfig` or `/sbin/ip addr` | #### Gateway Binding Fallbacks | Platform | Command | |----------|---------| | Linux | `ss -tlnp \| grep :18789` or `netstat -tlnp` | | macOS | `lsof -iTCP:18789 -sTCP:LISTEN` | | Windows | PowerShell: `Get-NetTCPConnection -LocalPort 18789` | #### File Permissions Fallbacks | Platform | Command | |----------|---------| | Linux/macOS | `ls -la ~/.openclaw` | | Windows | PowerShell: `Get-Acl $env:USERPROFILE\.openclaw` | ### Windows Native Support If `os` is `windows` and scripts fail completely: 1. Use PowerShell commands directly: ```powershell # Network exposure Get-NetTCPConnection -LocalPort 18789 -State Listen # File permissions Get-Acl "$env:USERPROFILE\.openclaw" # Process info Get-Process | Where-Object {$_.Name -like "*openclaw*"} ``` 2. Report what you can check and note Windows-specific limitations. ### Minimal Environments (Docker, DSM) Some environments lack tools. Check output and supplement: | Missing Tool | Fallback | |--------------|----------| | `curl` | `wget -qO-` | | `ss` | `netstat` | | `ip` | `ifconfig` or `/sbin/ip` | | `pgrep` | `ps aux \| grep` | ### Agent Decision Flow ``` 1. Run full_audit.sh 2. Check "failed_checks" in output 3. For each failed check: a. Identify platform from os/os_variant b. Run platform-specific fallback command c. Incorporate results into report 4. Note any checks that couldn't complete ``` ## Dashboard Generation When user requests visual report: ```bash bash ~/.openclaw/skills/secucheck/scripts/serve_dashboard.sh ``` Returns: ```json { "status": "ok", "url": "http://localhost:8766/secucheck-report.html", "pid": 12345 } ``` Provide URL directly to user. ## Detailed Check References Read these only when deep explanation needed: | File | Domain | |------|--------| | `checks/runtime.md` | Live system state | | `checks/channels.md` | Channel policies | | `checks/agents.md` | Agent permissions | | `checks/cron.md` | Scheduled jobs | | `checks/skills.md` | Installed skills | | `checks/sessions.md` | Session isolation | | `checks/network.md` | Network configuration | ## Attack Scenario Templates Use these for expert-level explanations: | File | Scenario | |------|----------| | `scenarios/prompt-injection.md` | External content manipulation | | `scenarios/session-leak.md` | Cross-session data exposure | | `scenarios/privilege-escalation.md` | Tool permission abuse | | `scenarios/credential-exposure.md` | Secret leakage | | `scenarios/unauthorized-access.md` | Access control bypass | ## Risk Levels ``` 🔴 Critical - Immediate action required. Active exploitation possible. 🟠 High - Significant risk. Should fix soon. 🟡 Medium - Notable concern. Plan to address. 🟢 Low - Minor issue or best practice recommendation. ⚪ Info - Not a risk, but worth noting. ``` ## Risk Matrix ``` Tool Permissions Minimal Full ┌──────────┬──────────┐ Exposure │ 🟢 │ 🟡 │ Low │ Safe │ Caution │ ├──────────┼──────────┤ │ 🟡 │ 🔴 │ High │ Caution │ Critical │ └──────────┴──────────┘ Exposure = Who can talk to the bot (DM policy, group access, public channels) Tool Permissions = What the bot can do (exec, file access, messaging, browser) ``` ## Context-Aware Exceptions Don't just pattern match. Consider context: | Context | Adjustment | |---------|------------| | Private channel, 2-3 trusted members | Lower risk even with exec | | VPN/Tailscale only access | Network exposure less critical | | Self-hosted, single user | Session isolation less important | | Containerized environment | Privilege escalation less severe | Always ask about environment if unclear. ## Applying Fixes **CRITICAL RULES:** 1. **Never auto-apply fixes.** Always show suggestions first. 2. **Warn about functional impact.** If a fix might break something, say so. 3. **Get explicit user confirmation** before any config changes. Example flow: ``` Agent: "Changing this setting will disable exec in #dev channel. If you're using code execution there, it will stop working. Apply this fix?" User: "yes" Agent: [apply fix via gateway config.patch] ``` ## Language Rules - **Internal processing**: Always English - **Thinking/reasoning**: Always English - **Final user-facing report**: Match user's language - **Technical terms**: Keep in English (exec, cron, gateway, etc.) ## Auto-Review Triggers Invoke automatically when: 1. **Skill installation**: `clawhub install <skill>` or manual addition 2. **Agent creation/modification**: New agent or tool changes 3. **Cron job creation/modification**: New or modified scheduled tasks For auto-reviews, focus only on changed component unless full audit requested. ## Quick Commands | User Request | Action | |--------------|--------| | "check channels only" | Run channels.md check | | "audit cron jobs" | Run cron.md check | | "full audit" | All checks | | "more detail" | Re-run with verbose output | ## Trust Hierarchy Apply appropriate trust levels: | Level | Entity | Trust Model | |-------|--------|-------------| | 1 | Owner | Full trust — has all access | | 2 | AI Agent | Trust but verify — sandboxed, logged | | 3 | Allowlists | Limited trust — specified users only | | 4 | Strangers | No trust — blocked by default | ## Incident Response Reference If compromise suspected: ### Containment 1. Stop gateway process 2. Set gateway.bind to loopback (127.0.0.1) 3. Disable risky DM/group policies ### Rotation 1. Regenerate gateway auth token 2. Rotate browser control tokens 3. Revoke and rotate API keys ### Review 1. Check gateway logs and session transcripts 2. Review recent config changes 3. Re-run full security audit ## Files Reference ``` ~/.openclaw/skills/secucheck/ ├── SKILL.md # This file ├── skill.json # Package metadata ├── README.md # User documentation ├── scripts/ │ ├── full_audit.sh # Complete audit (JSON output) │ ├── runtime_check.sh # Live system checks │ ├── gather_config.sh # Config extraction (redacted) │ ├── gather_skills.sh # Skill security scan │ ├── gather_agents.sh # Agent configurations │ ├── serve_dashboard.sh # Generate + serve HTML report │ └── generate_dashboard.sh ├── dashboard/ │ └── template.html # Dashboard template ├── checks/ │ ├── runtime.md # Runtime interpretation │ ├── channels.md # Channel policy checks │ ├── agents.md # Agent permission checks │ ├── cron.md # Cron job checks │ ├── skills.md # Skill safety checks │ ├── sessions.md # Session isolation │ └── network.md # Network exposure ├── scenarios/ │ ├── prompt-injection.md │ ├── session-leak.md │ ├── privilege-escalation.md │ ├── credential-exposure.md │ └── unauthorized-access.md └── templates/ ├── report.md # Full report template ├── finding.md # Single finding template └── summary.md # Quick summary template ``` ## Security Assessment Questions When auditing, consider: 1. **Exposure**: What network interfaces can reach this agent? 2. **Authentication**: What verification does each access point require? 3. **Isolation**: What boundaries exist between agent and host? 4. **Trust**: What content sources are considered "trusted"? 5. **Auditability**: What evidence exists of agent's actions? 6. **Least Privilege**: Does agent have only necessary permissions? --- **Remember:** This skill exists to make OpenClaw self-aware of its security posture. Use regularly, extend as needed, never skip the audit.

path: README.md
content: # 🔒 secucheck

**Comprehensive security audit skill for OpenClaw**

Analyzes configuration, permissions, exposure risks, and runtime environment with context-aware recommendations.

## Installation

```bash
clawhub install secucheck
```

## Usage

Ask your OpenClaw agent:

```
security audit
```

Or:
- `secucheck`
- `run security check`
- `audit my setup`

## Expertise Levels

On first run, you'll be asked to choose a level:

| Level | Description |
|-------|-------------|
| 🌱 Beginner | Simple analogies, no jargon |
| 💻 Intermediate | Technical details, config examples |
| 🔐 Expert | Attack vectors + edge cases |

All levels run identical checks—only explanation depth varies.

## What It Checks

- ⚡ **Runtime**: Network exposure, VPN, containers, privileges
- 📢 **Channels**: DM policies, group policies, mention settings
- 🤖 **Agents**: Tool permissions, workspace isolation
- ⏰ **Cron Jobs**: Automated tasks, external data dependencies
- 🧩 **Skills**: Installed skill security scan
- 🔐 **Sessions**: Session isolation, memory settings
- 🌐 **Network**: Gateway binding, authentication

## Dashboard

Visual HTML report:

```
show dashboard
```

Displays:
- Overall security score
- Runtime environment status
- Findings by severity with collapsible details

## Auto-Review

This skill runs automatically when:
- Installing new skills
- Creating/modifying agents
- Creating/modifying cron jobs

## Risk Levels

| Icon | Severity | Meaning |
|------|----------|---------|
| 🔴 | Critical | Immediate action required |
| 🟠 | High | Significant risk, fix soon |
| 🟡 | Medium | Notable concern |
| 🟢 | Low | Minor issue or best practice |
| ⚪ | Info | Not a risk, but notable |

## Context-Aware

secucheck considers your environment:
- VPN/Tailscale? Network findings less critical
- Single user? Session isolation less important
- Containerized? Privilege escalation less severe

## Example Output

```
🔒 Security Audit Results

🟢 Good

| Severity | Count |
|----------|-------|
| 🔴 Critical | 0 |
| 🟠 High | 0 |
| 🟡 Medium | 1 |
| 🟢 Low | 2 |

Runtime: VPN active ✅ | No container | sudo available
```

## Safe by Design

- **Read-only**: Never modifies configuration automatically
- **Explicit consent**: All fixes require user confirmation
- **Impact warnings**: Explains what might break before applying

## License

MIT

## Author

[joon](https://github.com/joon) & OpenClaw Agent


path: _meta.json
content: ownerId: kn7c92f305hxwpvjg5pb841qh580bw2m
slug: secucheck
version: 2.7.0
publishedAt: 1770530512015

path: checks/agents.md
content: # Agent Security Checks

## What to Examine

From `config.agents.list[]`, check each agent's permissions.

## Check 1: Tool Profile

**Location**: `agents.list[].tools.profile`

| Profile | Risk Level | Notes |
|---------|------------|-------|
| `minimal` | 🟢 Low | Read, basic tools only |
| `standard` | 🟡 Medium | Includes exec |
| `coding` | 🟡 Medium | Full coding capabilities |
| `full` | 🟠 High | Everything enabled |
| (none specified) | Check defaults | Inherits from `agents.defaults` |

## Check 2: Dangerous Tool Combinations

**Location**: `agents.list[].tools.alsoAllow` and `.deny`

**Critical tools to flag**:
- `exec` - Shell command execution
- `gateway` - Can modify bot configuration
- `browser` - Web browsing (prompt injection surface)
- `sessions_spawn` - Can create sub-agents
- `cron` - Can schedule persistent tasks
- `nodes` - Remote execution on paired devices

**Finding Pattern**:
```
Agent exposed to external input (non-main) + exec + no deny = 🔴 Critical
Agent with gateway access (non-main) = 🔴 Critical (can reconfigure itself)
Agent with browser + exec = 🟠 High (fetch-and-execute chain)
```

## Check 3: Workspace Sharing

**Location**: `agents.list[].workspace`

**Risk if multiple agents share workspace**:
- One compromised agent can affect others
- Cross-agent data leakage possible

**Check**:
- Count agents sharing same workspace path
- If >1 and any has external exposure: 🟡 Medium
- **Exception**: Single-user deployment = 🟢 Low (no cross-user risk)
- **Exception**: All agents are internal subagents = 🟢 Low

## Check 4: Subagent Permissions

**Location**: `agents.list[].subagents.allowAgents`

**Risk Assessment**:
- Main agent can spawn subagents listed here
- If subagent has more permissions than caller context: 🟡 Medium
- If subagent list includes `"*"` (all agents): 🟠 High

## Check 5: Message Tool as Exfiltration Vector

**Location**: `agents.list[].tools.alsoAllow`

**If agent has `message` tool**:
- Can send data to external channels
- Combined with file read = data exfiltration

**Risk Assessment**:
```
External-facing agent + message + Read = 🟠 High (exfiltration path)
Internal agent + message = 🟢 Low (expected for notifications)
```

## Check 6: Main Agent Special Risks

The `main` agent typically has:
- Direct user interaction
- Highest trust level
- Access to most tools

**Check main agent specifically for**:
- Overly broad `subagents.allowAgents`
- External channel connections without proper isolation

## Contextual Factors

Consider the agent's purpose:

1. **Coding agent** (like dev-expert)
   - exec is expected and necessary
   - Focus on channel exposure instead

2. **Data analyst agent**
   - Should NOT need exec or browser
   - Flag if present: 🟠 High

3. **External-facing agent** (public bot)
   - Should have minimal tools
   - Any exec/browser/gateway: 🔴 Critical


path: checks/channels.md
content: # Channel Security Checks

## What to Examine

From `config.channels.*`, check each enabled channel:

## Check 1: DM Policy

**Location**: `channels.<type>.dm.policy` or `channels.<type>.dmPolicy`

| Policy | Risk Level | Notes |
|--------|------------|-------|
| `disabled` | ⚪ None | DMs disabled |
| `pairing` | 🟢 Low | Default, requires approval |
| `allowlist` | 🟢 Low | Only pre-approved users |
| `open` | 🔴 Critical | Anyone can DM |

**Finding if `open`**:
- Scenario: `prompt-injection` or `unauthorized-access`
- Impact: Anyone on the internet can send commands to your bot
- Recommendation: Change to `pairing` or `allowlist`

## Check 2: Group Policy

**Location**: `channels.<type>.groupPolicy`

| Policy | Risk Level | Notes |
|--------|------------|-------|
| `disabled` | ⚪ None | Groups disabled |
| `allowlist` | 🟢 Low | Only listed groups |
| `open` | 🟠 High | Bot responds in any group it's added to |

**Finding if `open`**:
- Check if powerful tools (exec, browser, gateway) are available
- If yes: 🔴 Critical
- If minimal tools only: 🟡 Medium

## Check 3: Per-Channel Tool Overrides

**Location**: `channels.<type>.channels.<channelName>.tools`

Look for:
- `alsoAllow: ["exec", "browser", "gateway"]` in public/open channels
- Missing `deny` list for sensitive tools

**Risk Assessment**:
```
Channel is public/open + exec allowed = 🔴 Critical
Channel is allowlist + exec allowed = 🟡 Medium (depends on trust level)
Channel is private team + exec allowed = 🟢 Low (note it, don't alarm)
```

## Check 4: requireMention Setting

**Location**: `channels.<type>.channels.<channelName>.requireMention`

- `true`: Bot only responds when @mentioned
- `false`: Bot responds to all messages

**Risk**:
- `false` in public channels = 🟠 High (more attack surface)
- `false` in private channels = 🟢 Low (convenience feature)

## Check 5: allowBots Setting

**Location**: `channels.<type>.allowBots`

- `true`: Bot responds to other bots
- `false`: Ignores bot messages

**Risk if `true`**:
- ⚪ Info - Note this for awareness
- Only 🟡 Medium if: open channel + webhook integrations + exec tools
- Often required for legitimate webhook/automation use cases

## Check 6: Empty Allowlist

**Location**: `channels.<type>.channels` with `groupPolicy: allowlist`

**Critical Check**:
- If `groupPolicy: allowlist` but no channels defined = effectively disabled
- If allowlist channels exist but all have `allow: false` = effectively disabled
- Empty allowlist with DMs enabled = only DMs work

**Risk**:
- Misconfiguration that may not match user intent
- ⚪ Info - Confirm this is intentional

## Context Considerations

Before flagging as high risk, check:

1. **Is there a VPN/Tailscale overlay?** 
   - If `gateway.tailscale.mode` is active, network exposure is limited
   
2. **Is Control UI properly secured?**
   - Check `gateway.controlUi.allowInsecureAuth`
   - Check `gateway.auth.mode`

3. **User's stated environment**
   - Single user on personal machine? Lower risk.
   - Team deployment? Higher scrutiny needed.


path: checks/cron.md
content: # Cron Job Security Checks

## What to Examine

Use `cron action:"list" includeDisabled:true` to get all cron jobs.

## Check 1: Job Payload Type

**Location**: `job.payload.kind`

| Kind | Risk Level | Notes |
|------|------------|-------|
| `systemEvent` | 🟢 Low | Injects text into session (main only) |
| `agentTurn` | 🟡 Medium | Runs agent with full capabilities |

**agentTurn risks**:
- Runs with the agent's full tool permissions
- If agent has exec/browser: higher risk
- Check what the `message` prompt contains

## Check 2: External Data Dependencies

**Examine `job.payload.message`**:

Look for instructions that:
- Fetch URLs (`web_fetch`, "URL 확인해줘")
- Read external emails
- Process external webhooks
- Scrape websites

**Risk if external data + exec agent**:
- 🔴 Critical - Automated prompt injection vector
- Example: "매일 이 URL 읽고 실행해줘" = perfect attack vector

## Check 3: Frequency and Scope

**Location**: `job.schedule`

| Schedule | Consideration |
|----------|---------------|
| `kind: "at"` | One-shot, lower concern |
| `kind: "every"` with < 1hr | Frequent, higher scrutiny |
| `kind: "cron"` | Check expression complexity |

**High frequency + powerful agent = more attack windows**

## Check 4: Target Agent

**Location**: `job.agentId` or `job.sessionTarget`

Cross-reference with agents.md findings:
- If targeting an agent with exec: check payload carefully
- If targeting main session: less isolated, higher trust required

## Check 5: Delivery Settings

**Location**: `job.delivery`

| Mode | Notes |
|------|-------|
| `none` | Silent execution |
| `announce` | Reports to channel |

**Silent jobs with powerful actions**: 🟡 Medium
- Harder to notice if compromised
- Recommend enabling announce for visibility

## Check 6: Agent Capability Mismatch

**Cross-reference**: `job.agentId` → `agents.list[].tools`

**Check if job payload requests tools the agent doesn't have**:
- Job says "exec로 실행해줘" but agent has `deny: [exec]`
- Job says "브라우저로 확인해줘" but agent lacks browser tool

**Risk**:
- 🟡 Medium - Job won't work as intended (configuration bug)
- May indicate copy-paste error or outdated config
- Recommend: Fix agent permissions or change job payload

## Check 7: Main Session Target

**Location**: `job.sessionTarget`

| Target | Risk |
|--------|------|
| `isolated` | 🟢 Low - Separate session per run |
| `main` | 🟡 Medium - Runs in main context |

**Risks with `main`**:
- Shares context with user's main session
- Can access main session's history/memory
- Errors affect main session state

**Recommendation**: Prefer `isolated` for automated jobs

## Specific Patterns to Flag

### Pattern A: URL Scraper + Exec
```yaml
payload:
  message: "이 URL 내용 분석하고 필요한 조치 취해줘"
  # + agent has exec tool
```
**Risk**: 🔴 Critical - Trivial prompt injection

### Pattern B: Email Processor
```yaml
payload:
  message: "새 이메일 확인하고 처리해줘"
```
**Risk**: 🟠 High - Email-based injection possible

### Pattern C: Monitoring with Actions
```yaml
payload:
  message: "서버 상태 확인하고 문제 있으면 조치해줘"
```
**Risk**: 🟡 Medium - Automated actions need guardrails

## Recommended Mitigations

For risky cron jobs, suggest:

1. **Separate agent**: Create a minimal-tool agent for the cron task
2. **Read-only first**: Fetch and report, don't auto-execute
3. **Allowlist actions**: Specify exactly what actions are permitted
4. **Enable announce**: Make job results visible


path: checks/network.md
content: # Network Security Checks

## What to Examine

Network-related settings from config.

## Check 1: Gateway Binding

**Location**: `gateway.bind`

| Value | Risk Level | Notes |
|-------|------------|-------|
| `localhost` / `127.0.0.1` | 🟢 Low | Local only |
| `lan` | 🟡 Medium | LAN accessible |
| `0.0.0.0` | 🟠 High | All interfaces |
| (specific IP) | Varies | Check if public |

**If binding to non-localhost**:
- Check authentication is enabled
- Check if behind firewall/VPN

## Check 2: Gateway Authentication

**Location**: `gateway.auth`

| Mode | Risk Level | Notes |
|------|------------|-------|
| `token` | 🟢 Good | Requires token |
| `password` | 🟢 Good | Requires password |
| `none` / disabled | 🔴 Critical | No auth |

**Weak token check** (entropy-based):
- Token is common word/phrase: 🔴 Critical
- Token < 16 chars with low entropy: 🟠 High
- Token 16-24 chars with high entropy: 🟢 OK
- Token 24+ chars: 🟢 OK regardless

**Note**: A high-entropy 24-char token is fine. Don't flag based on length alone.

## Check 3: Control UI Security

**Location**: `gateway.controlUi`

| Setting | Risk |
|---------|------|
| `allowInsecureAuth: true` | 🟡 Medium - No device pairing |
| `dangerouslyDisableDeviceAuth: true` | 🔴 Critical |
| Missing `allowedOrigins` | 🟡 Medium |

**HTTPS check**:
- Control UI over HTTP (not localhost): 🟠 High
- Token visible in URL parameters: 🟡 Medium

## Check 4: Tailscale Configuration

**Location**: `gateway.tailscale`

| Mode | Notes |
|------|-------|
| `off` | Not using Tailscale |
| `serve` | Tailscale serving (good for security) |
| `funnel` | Public exposure via Tailscale |

**If funnel enabled**:
- Bot accessible from internet
- Verify authentication is strong
- 🟠 High if combined with powerful tools

## Check 5: Trusted Proxies

**Location**: `gateway.trustedProxies`

**If behind reverse proxy**:
- Should list proxy IPs
- Missing config = IP spoofing possible

**No trustedProxies + reverse proxy detected**: 🟡 Medium

## Check 6: Plugin Network Access

**Location**: `plugins.entries`

Some plugins may open additional network surfaces:
- `voice-call`: Opens telephony connection
- Custom plugins: Check their network usage

## Context-Aware Assessment

### VPN/Private Network Verification

Don't just trust user claims. Do a quick check:

```bash
# Check for VPN interfaces
ip link | grep -E "wg[0-9]|tailscale|tun[0-9]"

# Check if Tailscale is active
tailscale status 2>/dev/null | head -1

# Check for WireGuard
wg show 2>/dev/null | head -1
```

**If VPN detected**:
- Reduce severity of LAN binding warnings
- Network exposure less critical

**If no VPN but user claims private network**:
- Note the discrepancy
- Still reduce severity but mention assumption

**Always check regardless of VPN**:
- Authentication enabled
- Control UI security
- Token strength (entropy-based, not length-based)

### User claims single-user personal use:

**Reduce concern for**:
- Session isolation (only one user)
- DM scope settings

**Still important**:
- Auth tokens
- Tool permissions (self-protection)

## Quick Network Audit Commands

Suggest user run (if they want to verify):

```bash
# Check what's listening
ss -tlnp | grep -E "(18789|openclaw)"

# Check firewall status
sudo ufw status

# Check if port is accessible externally
# (only if user consents to external check)
```

## Recommendations by Risk

| Finding | Recommendation |
|---------|----------------|
| No auth + LAN bind | Enable token or password auth immediately |
| Public exposure | Use Tailscale Serve instead of direct exposure |
| Weak token | Generate new token: `openclaw gateway token --rotate` |
| Control UI insecure | Switch to HTTPS or Tailscale |


path: checks/runtime.md
content: # Runtime Security Checks

## Data Source

Run `scripts/runtime_check.sh` to gather live system state.

## Check 1: Network Exposure

**From**: `runtime.network`

### Direct Internet Exposure

| Condition | Risk |
|-----------|------|
| `potentially_exposed: true` + `behind_nat: false` | 🔴 Critical |
| `potentially_exposed: true` + `behind_nat: true` | 🟡 Medium (NAT helps) |
| `potentially_exposed: false` | 🟢 Low |

**Critical Finding** (`exposed + no NAT`):
```
Gateway is binding to all interfaces (0.0.0.0) and your external IP 
matches a local IP, meaning you may be directly exposed to the internet 
without NAT protection.

Immediate actions:
1. Check if port {gateway_port} is reachable from outside
2. Enable firewall: sudo ufw allow from <trusted_ip> to any port {gateway_port}
3. Or change gateway.bind to "localhost" or specific LAN IP
```

### VPN Status

| Condition | Impact |
|-----------|--------|
| `vpn_type: wireguard/tailscale` | Reduces network exposure severity |
| `vpn_type: none` + LAN bind | 🟡 Medium (depends on network trust) |
| `vpn_type: none` + 0.0.0.0 bind | 🟠 High |

## Check 2: Container/VM Isolation

**From**: `runtime.isolation`

### Bare Metal vs Container

| Environment | Security Implication |
|-------------|---------------------|
| `in_container: true` | 🟢 Some isolation from host |
| `in_container: false` + `in_vm: true` | 🟢 VM-level isolation |
| `in_container: false` + `in_vm: false` | ⚠️ Bare metal - full system access |

**Bare Metal Finding**:
```
OpenClaw is running directly on the host system without container 
or VM isolation. If compromised, an attacker has full access to:
- All files on the system
- Network interfaces
- Other running processes
- Potentially other users' data

Consider:
- Running in Docker for isolation
- Using a dedicated VM
- At minimum, running as non-root user with limited sudo
```

## Check 3: Privilege Level

**From**: `runtime.privileges`

### Root Access

| Condition | Risk |
|-----------|------|
| `running_as_root: true` | 🔴 Critical |
| `can_sudo: true` (passwordless) | 🟠 High |
| `can_sudo: true` (with password) | 🟡 Medium |
| `can_sudo: false` | 🟢 Low |

**Running as Root Finding**:
```
🔴 CRITICAL: OpenClaw is running as root!

This means any prompt injection that triggers exec can:
- Modify any file on the system
- Install rootkits/backdoors
- Access all users' data
- Modify system configuration

Immediate action: Run OpenClaw as a non-root user.
```

**Passwordless Sudo Finding**:
```
🟠 HIGH: OpenClaw user can run sudo without password.

An attacker who achieves command execution can escalate to root.
Consider restricting sudo access or requiring password.
```

### Capabilities Check

If `capabilities` is not "none":
- Check which capabilities are granted
- `CAP_NET_ADMIN`, `CAP_SYS_ADMIN` = 🟠 High risk
- `CAP_NET_BIND_SERVICE` only = 🟢 Low (common for ports <1024)

## Check 4: File Permissions

**From**: `runtime.filesystem`

### OpenClaw Directory

| Condition | Risk |
|-----------|------|
| `openclaw_dir_perms` not 700 | 🟡 Medium |
| `config_perms` not 600 | 🟡 Medium |
| `credentials_dir_perms` not 700 | 🟠 High |
| `world_readable_sensitive_files > 0` | 🟠 High |

**Permissions Finding**:
```
Sensitive files are readable by other users on this system.

Files in ~/.openclaw may contain:
- API tokens (Anthropic, OpenAI)
- Channel tokens (Slack, Telegram, Discord)
- Gateway authentication credentials

Fix:
chmod 700 ~/.openclaw
chmod 600 ~/.openclaw/openclaw.json
chmod -R 700 ~/.openclaw/credentials
```

## Check 5: Resource Limits

**From**: `runtime.resources`

Informational checks for DoS resilience:

| Resource | Concern |
|----------|---------|
| `max_open_files` < 1024 | May limit concurrent connections |
| `max_processes` < 100 | May limit subagent spawning |
| `disk_free_mb` < 1000 | Session logs may fill disk |

These are ⚪ Info level unless extremely constrained.

## Risk Summary Matrix

```
                    Isolation Level
                 Container    Bare Metal
            ┌────────────┬────────────┐
Privileges  │            │            │
   Root     │  🟠 High   │ 🔴 Critical│
            ├────────────┼────────────┤
   Sudo     │  🟡 Medium │  🟠 High   │
            ├────────────┼────────────┤
   Limited  │  🟢 Low    │  🟡 Medium │
            └────────────┴────────────┘
```

## Combined Assessment

Generate an overall runtime security score:

1. **Network Exposure**: Exposed + No VPN + No NAT = 🔴
2. **Privilege Level**: Root or passwordless sudo = 🔴/🟠
3. **Isolation**: Bare metal + root = 🔴
4. **File Permissions**: World-readable creds = 🟠

If any 🔴: Overall = 🔴 Critical
If any 🟠 and no 🔴: Overall = 🟠 High
If only 🟡: Overall = 🟡 Medium
Otherwise: 🟢 Good


path: checks/sessions.md
content: # Session Security Checks

## What to Examine

Session configuration from `config.sessions` and `config.agents.defaults`.

## Check 1: DM Scope

**Location**: `sessions.dmScope` or default behavior

| Scope | Risk Level | Notes |
|-------|------------|-------|
| `main` | Depends | All DMs share context (default) |
| `per-peer` | 🟢 Low | Isolated by sender |
| `per-channel-peer` | 🟢 Low | Isolated by channel+sender |
| `per-account-channel-peer` | 🟢 Low | Most isolated |

**Risk with `main` scope**:
- Multiple users DM the same bot = shared context
- User A's secrets visible to User B

**Context-Aware Assessment**:
```
Single user (personal deployment) + main scope = 🟢 Low (no risk)
Multi-user + main scope = 🔴 Critical (session leak)
Unknown user count + main scope = 🟡 Medium (ask user)
```

**Finding if multi-user + main scope**:
- Scenario: `session-leak`
- Recommendation: Switch to `per-peer` or more specific

## Check 2: Session Memory

**Location**: `agents.defaults.memorySearch`

| Setting | Consideration |
|---------|---------------|
| `enabled: true` | Sessions can search past conversations |
| `provider: local` | Memory stored locally |
| `provider: api` | Memory sent to external API |

**Risks**:
- Memory search across sessions could leak data
- External provider = data leaves your control

## Check 3: Workspace per Session

**Location**: `agents.list[].workspace`

**Check if sessions share workspace**:
- Files created in one session accessible to others
- Credentials, notes, generated content could leak

**Single workspace for all agents**: 🟡 Medium
- Recommend separate workspaces for external-facing agents

## Check 4: Session Persistence

**Location**: `~/.openclaw/agents/<agentId>/sessions/`

**Check**:
- Old session logs accumulating
- Sensitive data in session history
- Permissions on session directories

**If sessions contain sensitive data**:
- Recommend periodic cleanup
- Verify `chmod 600` on session files

## Check 5: Context Pruning

**Location**: `agents.defaults.contextPruning`

| Mode | Notes |
|------|-------|
| `none` | Full context kept |
| `cache-ttl` | Prunes after time |
| (others) | Various strategies |

**No pruning + long sessions**:
- Old context may contain sensitive data
- Larger attack surface for context manipulation

## Multi-User Environment Checks

If the deployment serves multiple users:

### A. Session Isolation Verification
- Each user should have isolated sessions
- Test: Can user A's data appear in user B's context?

### B. Workspace Isolation
- Users should not share workspace
- Or workspace should have per-user subdirectories

### C. Memory Isolation
- Memory search should be scoped to user
- Check if cross-user memory access is possible

## Recommendations

| Scenario | Recommended Config |
|----------|-------------------|
| Single user, personal | `main` scope OK |
| Multi-user, same team | `per-peer` minimum |
| Public bot | `per-channel-peer` + separate workspace |
| Enterprise | Full isolation + audit logging |


path: checks/skills.md
content: # Skill Security Checks

## What to Examine

Run `gather_skills.sh` to get installed skills list.

## Check 1: Skill Source

**Categories**:

| Source | Trust Level | Notes |
|--------|-------------|-------|
| Builtin (OpenClaw) | 🟢 High | Shipped with OpenClaw |
| ClawHub (verified) | 🟢 Medium-High | Community reviewed |
| ClawHub (unverified) | 🟡 Medium | Use caution |
| Manual install | 🟡 Variable | Depends on source |
| Unknown origin | 🟠 Low | Investigate |

**Check for each user skill**:
- Is there a clear source/author?
- Was it installed via clawhub or manually copied?
- When was it last updated?

## Check 2: Skill Capabilities

**From SKILL.md, look for**:

| Capability | Risk Indicator |
|------------|----------------|
| `exec`, `shell`, `bash` | Runs system commands |
| `Write`, `Edit` | Modifies files |
| `browser` | Web browsing |
| `gateway` | Config modification |
| `cron` | Scheduling |
| `message` | External messaging |
| External API calls | Data exfiltration potential |

**Risk Matrix**:
```
Unknown source + exec capability = 🔴 Critical
Known source + exec capability = 🟡 Medium (review script contents)
Any source + read-only = 🟢 Low
```

## Check 3: Script Contents (Automated Scan)

For skills with scripts directory, actually scan file contents:

```bash
# Run on each script file in skill/scripts/
grep -l "curl\|wget" *.sh *.py *.js 2>/dev/null  # Network calls
grep -l "base64\|xxd\|od -x" *.sh 2>/dev/null     # Encoding (obfuscation)
grep -l "\.ssh\|\.aws\|\.config" *.sh 2>/dev/null  # Sensitive paths
grep -l "rm -rf\|mkfs\|dd if=" *.sh 2>/dev/null   # Destructive commands
```

**Red flags to report**:
| Pattern | Risk | Action |
|---------|------|--------|
| `curl/wget` + external URL | 🟡 Medium | Show the URL, let user decide |
| `base64 -d` in script | 🟠 High | Could hide malicious payload |
| Access to `~/.ssh`, `~/.aws` | 🟠 High | Credential theft risk |
| `rm -rf /` or similar | 🔴 Critical | Block immediately |
| Hardcoded IP addresses | 🟡 Medium | Potential C2 server |

**Do NOT flag**:
- `curl` to localhost or known APIs (github.com, api.openai.com, etc.)
- Standard package manager calls (npm, pip, apt)
- Example code in comments

## Check 4: Skill Metadata

If skill has `skill.json` or similar:

**Check for**:
- `minClawdVersion` - Compatibility
- `permissions` - Declared requirements
- `author`, `repository` - Traceability

**Missing metadata**: 🟡 Medium - Harder to audit

## Check 5: Skills Without Metadata

Skills missing `skill.json` or proper metadata:
- No version tracking
- No author/source info
- Harder to verify integrity

**Action**: ⚪ Info - Note for awareness, not a security issue per se

## Specific Skill Patterns

### Pattern A: Coding Helper
```
Skills that help with coding (happy, claude-code, etc.)
- Expected to have exec
- Focus on source trust
```

### Pattern B: Web Scrapers
```
Skills that fetch and process web content
- High injection surface
- Check if results go to exec
```

### Pattern C: Automation Skills
```
Skills that automate workflows
- Often need multiple powerful tools
- Verify each tool is necessary
```

## Recommendations by Finding

| Finding | Recommendation |
|---------|----------------|
| Unknown source skill | Review SKILL.md and scripts before use |
| Exec in web-related skill | Audit data flow carefully |
| No SKILL.md | Consider removing or replacing |
| Outdated skill | Check for updates on ClawHub |

## Auto-Review Trigger

When a new skill is installed:
1. Run `gather_skills.sh` 
2. Identify the new skill
3. Apply checks above to the new skill only
4. Report findings to user before first use


path: dashboard/template-en.html
content: <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SecuCheck Security Audit</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Inter', -apple-system, sans-serif; background: #111; color: #ddd; font-size: 16px; line-height: 1.6; }
        .container { max-width: 960px; margin: 0 auto; padding: 40px 24px; }
        header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 32px; padding-bottom: 20px; border-bottom: 1px solid #333; }
        h1 { font-size: 20px; font-weight: 600; display: flex; align-items: center; gap: 10px; }
        .meta { color: #777; font-size: 14px; font-family: 'JetBrains Mono', monospace; }
        .summary { display: grid; grid-template-columns: 120px 1fr; gap: 24px; margin-bottom: 32px; }
        .score { background: #1a1a1a; border: 1px solid #333; padding: 24px; text-align: center; }
        .score-icon { font-size: 48px; }
        .score-label { font-size: 14px; font-weight: 500; margin-top: 8px; color: #999; }
        .counts { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; }
        .count-box { background: #1a1a1a; border: 1px solid #333; padding: 16px; text-align: center; }
        .count-num { font-size: 28px; font-weight: 600; font-family: 'JetBrains Mono', monospace; }
        .count-label { font-size: 12px; color: #777; margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
        .critical .count-num { color: #f66; }
        .high .count-num { color: #fa5; }
        .medium .count-num { color: #fc3; }
        .low .count-num { color: #6c6; }
        .info .count-num { color: #888; }
        .section { background: #1a1a1a; border: 1px solid #333; margin-bottom: 16px; }
        .section-header { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; cursor: pointer; user-select: none; }
        .section-header:hover { background: #222; }
        .section-title { font-size: 15px; font-weight: 500; display: flex; align-items: center; gap: 10px; }
        .section-title span { font-size: 18px; }
        .badge { font-size: 12px; font-weight: 500; padding: 4px 10px; font-family: 'JetBrains Mono', monospace; }
        .badge-ok { color: #6c6; }
        .badge-low { color: #6c6; }
        .badge-medium { color: #fc3; }
        .badge-high { color: #fa5; }
        .badge-critical { color: #f66; }
        .section-content { display: none; padding: 20px; border-top: 1px solid #333; }
        .section.open .section-content { display: block; }
        .arrow { transition: transform 0.2s; color: #555; }
        .section.open .arrow { transform: rotate(180deg); }
        .runtime-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; }
        .runtime-card h4 { font-size: 12px; color: #666; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 500; }
        .runtime-item { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #2a2a2a; font-size: 14px; }
        .runtime-item:last-child { border-bottom: none; }
        .val { font-family: 'JetBrains Mono', monospace; font-size: 13px; }
        .val.good { color: #6c6; }
        .val.warn { color: #fc3; }
        .val.bad { color: #f66; }
        .findings-list { list-style: none; }
        .finding-item { padding: 14px 16px; margin-bottom: 10px; background: #111; border-left: 3px solid #333; font-size: 14px; }
        .finding-item:last-child { margin-bottom: 0; }
        .finding-item.critical { border-left-color: #f66; }
        .finding-item.high { border-left-color: #fa5; }
        .finding-item.medium { border-left-color: #fc3; }
        .finding-item.low { border-left-color: #6c6; }
        .finding-item.info { border-left-color: #666; }
        .finding-item small { display: block; color: #888; margin-top: 6px; font-size: 13px; }
        footer { text-align: center; padding: 32px 0; color: #555; font-size: 13px; }
        footer a { color: #888; text-decoration: none; }
        footer a:hover { color: #aaa; }
        @media (max-width: 700px) { .summary { grid-template-columns: 1fr; } .counts { grid-template-columns: repeat(3, 1fr); } }
        @media (max-width: 500px) { .counts { grid-template-columns: repeat(2, 1fr); } .count-box:nth-child(5) { grid-column: span 2; } }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>🔒 SecuCheck</h1>
            <div class="meta">{{SCAN_TIME}}<br>{{HOSTNAME}}</div>
        </header>
        <div class="summary">
            <div class="score">
                <div class="score-icon">{{SCORE_EMOJI}}</div>
                <div class="score-label">{{SCORE_LABEL}}</div>
            </div>
            <div class="counts">
                <div class="count-box critical"><div class="count-num">{{COUNT_CRITICAL}}</div><div class="count-label">Critical</div></div>
                <div class="count-box high"><div class="count-num">{{COUNT_HIGH}}</div><div class="count-label">High</div></div>
                <div class="count-box medium"><div class="count-num">{{COUNT_MEDIUM}}</div><div class="count-label">Medium</div></div>
                <div class="count-box low"><div class="count-num">{{COUNT_LOW}}</div><div class="count-label">Low</div></div>
                <div class="count-box info"><div class="count-num">{{COUNT_INFO}}</div><div class="count-label">Info</div></div>
            </div>
        </div>
        <div class="section open" id="section-runtime">
            <div class="section-header" onclick="toggle('runtime')">
                <span class="section-title"><span>⚡</span> Runtime Environment</span>
                <div style="display:flex;align-items:center;gap:12px;">
                    <span class="badge {{RUNTIME_BADGE}}">{{RUNTIME_STATUS}}</span>
                    <span class="arrow">▼</span>
                </div>
            </div>
            <div class="section-content"><div class="runtime-grid">{{RUNTIME_CONTENT}}</div></div>
        </div>
        {{FINDINGS_SECTIONS}}
        <footer><a href="https://clawhub.ai/jooneyp/secucheck">secucheck</a> · Re-run: "security audit"</footer>
    </div>
    <script>
        function toggle(id) { document.getElementById('section-' + id).classList.toggle('open'); }
        document.querySelectorAll('.section').forEach(s => { if (s.querySelector('.badge-ok')) s.classList.remove('open'); });
    </script>
</body>
</html>


path: dashboard/template-ja.html
content: <!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SecuCheck セキュリティ監査</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&family=Noto+Sans+JP:wght@400;500&display=swap" rel="stylesheet">
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Noto Sans JP', 'Inter', sans-serif; background: #111; color: #ddd; font-size: 16px; line-height: 1.6; }
        .container { max-width: 960px; margin: 0 auto; padding: 40px 24px; }
        header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 32px; padding-bottom: 20px; border-bottom: 1px solid #333; }
        h1 { font-size: 20px; font-weight: 600; display: flex; align-items: center; gap: 10px; }
        .meta { color: #777; font-size: 14px; font-family: 'JetBrains Mono', monospace; }
        .summary { display: grid; grid-template-columns: 120px 1fr; gap: 24px; margin-bottom: 32px; }
        .score { background: #1a1a1a; border: 1px solid #333; padding: 24px; text-align: center; }
        .score-icon { font-size: 48px; }
        .score-label { font-size: 14px; font-weight: 500; margin-top: 8px; color: #999; }
        .counts { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; }
        .count-box { background: #1a1a1a; border: 1px solid #333; padding: 16px; text-align: center; }
        .count-num { font-size: 28px; font-weight: 600; font-family: 'JetBrains Mono', monospace; }
        .count-label { font-size: 12px; color: #777; margin-top: 4px; }
        .critical .count-num { color: #f66; }
        .high .count-num { color: #fa5; }
        .medium .count-num { color: #fc3; }
        .low .count-num { color: #6c6; }
        .info .count-num { color: #888; }
        .section { background: #1a1a1a; border: 1px solid #333; margin-bottom: 16px; }
        .section-header { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; cursor: pointer; user-select: none; }
        .section-header:hover { background: #222; }
        .section-title { font-size: 15px; font-weight: 500; display: flex; align-items: center; gap: 10px; }
        .badge { font-size: 12px; font-weight: 500; padding: 4px 10px; font-family: 'JetBrains Mono', monospace; }
        .badge-ok { color: #6c6; } .badge-low { color: #6c6; } .badge-medium { color: #fc3; } .badge-high { color: #fa5; } .badge-critical { color: #f66; }
        .section-content { display: none; padding: 20px; border-top: 1px solid #333; }
        .section.open .section-content { display: block; }
        .arrow { transition: transform 0.2s; color: #555; }
        .section.open .arrow { transform: rotate(180deg); }
        .runtime-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; }
        .runtime-card h4 { font-size: 12px; color: #666; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
        .runtime-item { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #2a2a2a; font-size: 14px; }
        .runtime-item:last-child { border-bottom: none; }
        .val { font-family: 'JetBrains Mono', monospace; font-size: 13px; }
        .val.good { color: #6c6; } .val.warn { color: #fc3; } .val.bad { color: #f66; }
        .findings-list { list-style: none; }
        .finding-item { padding: 14px 16px; margin-bottom: 10px; background: #111; border-left: 3px solid #333; font-size: 14px; }
        .finding-item.critical { border-left-color: #f66; } .finding-item.high { border-left-color: #fa5; } .finding-item.medium { border-left-color: #fc3; } .finding-item.low { border-left-color: #6c6; } .finding-item.info { border-left-color: #666; }
        .finding-item small { display: block; color: #888; margin-top: 6px; font-size: 13px; }
        footer { text-align: center; padding: 32px 0; color: #555; font-size: 13px; }
        footer a { color: #888; }
        @media (max-width: 700px) { .summary { grid-template-columns: 1fr; } .counts { grid-template-columns: repeat(3, 1fr); } }
    </style>
</head>
<body>
    <div class="container">
        <header><h1>🔒 SecuCheck</h1><div class="meta">{{SCAN_TIME}}<br>{{HOSTNAME}}</div></header>
        <div class="summary">
            <div class="score"><div class="score-icon">{{SCORE_EMOJI}}</div><div class="score-label">{{SCORE_LABEL}}</div></div>
            <div class="counts">
                <div class="count-box critical"><div class="count-num">{{COUNT_CRITICAL}}</div><div class="count-label">重大</div></div>
                <div class="count-box high"><div class="count-num">{{COUNT_HIGH}}</div><div class="count-label">高</div></div>
                <div class="count-box medium"><div class="count-num">{{COUNT_MEDIUM}}</div><div class="count-label">中</div></div>
                <div class="count-box low"><div class="count-num">{{COUNT_LOW}}</div><div class="count-label">低</div></div>
                <div class="count-box info"><div class="count-num">{{COUNT_INFO}}</div><div class="count-label">情報</div></div>
            </div>
        </div>
        <div class="section open" id="section-runtime">
            <div class="section-header" onclick="toggle('runtime')">
                <span class="section-title"><span>⚡</span> ランタイム環境</span>
                <div style="display:flex;align-items:center;gap:12px;"><span class="badge {{RUNTIME_BADGE}}">{{RUNTIME_STATUS}}</span><span class="arrow">▼</span></div>
            </div>
            <div class="section-content"><div class="runtime-grid">{{RUNTIME_CONTENT}}</div></div>
        </div>
        {{FINDINGS_SECTIONS}}
        <footer><a href="https://clawhub.ai/jooneyp/secucheck">secucheck</a> · 再実行: "セキュリティ監査"</footer>
    </div>
    <script>
        function toggle(id) { document.getElementById('section-' + id).classList.toggle('open'); }
        document.querySelectorAll('.section').forEach(s => { if (s.querySelector('.badge-ok')) s.classList.remove('open'); });
    </script>
</body>
</html>


path: dashboard/template-ko.html
content: <!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SecuCheck 보안 점검</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        
        body {
            font-family: 'Inter', -apple-system, sans-serif;
            background: #111;
            color: #ddd;
            font-size: 16px;
            line-height: 1.6;
        }
        
        .container { max-width: 960px; margin: 0 auto; padding: 40px 24px; }
        
        header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 32px;
            padding-bottom: 20px;
            border-bottom: 1px solid #333;
        }
        
        h1 { font-size: 20px; font-weight: 600; display: flex; align-items: center; gap: 10px; }
        .meta { color: #777; font-size: 14px; font-family: 'JetBrains Mono', monospace; }
        
        .summary {
            display: grid;
            grid-template-columns: 120px 1fr;
            gap: 24px;
            margin-bottom: 32px;
        }
        
        .score {
            background: #1a1a1a;
            border: 1px solid #333;
            padding: 24px;
            text-align: center;
        }
        
        .score-icon { font-size: 48px; }
        .score-label { font-size: 14px; font-weight: 500; margin-top: 8px; color: #999; }
        
        .counts {
            display: grid;
            grid-template-columns: repeat(5, 1fr);
            gap: 12px;
        }
        
        .count-box {
            background: #1a1a1a;
            border: 1px solid #333;
            padding: 16px;
            text-align: center;
        }
        
        .count-num {
            font-size: 28px;
            font-weight: 600;
            font-family: 'JetBrains Mono', monospace;
        }
        
        .count-label { font-size: 12px; color: #777; margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
        
        .critical .count-num { color: #f66; }
        .high .count-num { color: #fa5; }
        .medium .count-num { color: #fc3; }
        .low .count-num { color: #6c6; }
        .info .count-num { color: #888; }
        
        .section {
            background: #1a1a1a;
            border: 1px solid #333;
            margin-bottom: 16px;
        }
        
        .section-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 16px 20px;
            cursor: pointer;
            user-select: none;
        }
        
        .section-header:hover { background: #222; }
        
        .section-title {
            font-size: 15px;
            font-weight: 500;
            display: flex;
            align-items: center;
            gap: 10px;
        }
        
        .section-title span { font-size: 18px; }
        
        .badge {
            font-size: 12px;
            font-weight: 500;
            padding: 4px 10px;
            font-family: 'JetBrains Mono', monospace;
        }
        
        .badge-ok { color: #6c6; }
        .badge-low { color: #6c6; }
        .badge-medium { color: #fc3; }
        .badge-high { color: #fa5; }
        .badge-critical { color: #f66; }
        
        .section-content {
            display: none;
            padding: 20px;
            border-top: 1px solid #333;
        }
        
        .section.open .section-content { display: block; }
        
        .arrow {
            transition: transform 0.2s;
            color: #555;
        }
        
        .section.open .arrow { transform: rotate(180deg); }
        
        .runtime-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 20px;
        }
        
        .runtime-card h4 {
            font-size: 12px;
            color: #666;
            margin-bottom: 12px;
            text-transform: uppercase;
            letter-spacing: 0.5px;
            font-weight: 500;
        }
        
        .runtime-item {
            display: flex;
            justify-content: space-between;
            padding: 8px 0;
            border-bottom: 1px solid #2a2a2a;
            font-size: 14px;
        }
        
        .runtime-item:last-child { border-bottom: none; }
        
        .val {
            font-family: 'JetBrains Mono', monospace;
            font-size: 13px;
        }
        
        .val.good { color: #6c6; }
        .val.warn { color: #fc3; }
        .val.bad { color: #f66; }
        
        .findings-list { list-style: none; }
        
        .finding-item {
            padding: 14px 16px;
            margin-bottom: 10px;
            background: #111;
            border-left: 3px solid #333;
            font-size: 14px;
        }
        
        .finding-item:last-child { margin-bottom: 0; }
        
        .finding-item.critical { border-left-color: #f66; }
        .finding-item.high { border-left-color: #fa5; }
        .finding-item.medium { border-left-color: #fc3; }
        .finding-item.low { border-left-color: #6c6; }
        .finding-item.info { border-left-color: #666; }
        
        .finding-item small {
            display: block;
            color: #888;
            margin-top: 6px;
            font-size: 13px;
        }
        
        footer {
            text-align: center;
            padding: 32px 0;
            color: #555;
            font-size: 13px;
        }
        
        footer a { color: #888; text-decoration: none; }
        footer a:hover { color: #aaa; }
        
        @media (max-width: 700px) {
            .summary { grid-template-columns: 1fr; }
            .counts { grid-template-columns: repeat(3, 1fr); }
            .count-box:nth-child(4), .count-box:nth-child(5) { grid-column: span 1; }
        }
        
        @media (max-width: 500px) {
            .counts { grid-template-columns: repeat(2, 1fr); }
            .count-box:nth-child(5) { grid-column: span 2; }
        }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>🔒 SecuCheck</h1>
            <div class="meta">{{SCAN_TIME}}<br>{{HOSTNAME}}</div>
        </header>
        
        <div class="summary">
            <div class="score">
                <div class="score-icon">{{SCORE_EMOJI}}</div>
                <div class="score-label">{{SCORE_LABEL}}</div>
            </div>
            <div class="counts">
                <div class="count-box critical"><div class="count-num">{{COUNT_CRITICAL}}</div><div class="count-label">심각</div></div>
                <div class="count-box high"><div class="count-num">{{COUNT_HIGH}}</div><div class="count-label">높음</div></div>
                <div class="count-box medium"><div class="count-num">{{COUNT_MEDIUM}}</div><div class="count-label">중간</div></div>
                <div class="count-box low"><div class="count-num">{{COUNT_LOW}}</div><div class="count-label">낮음</div></div>
                <div class="count-box info"><div class="count-num">{{COUNT_INFO}}</div><div class="count-label">참고</div></div>
            </div>
        </div>
        
        <div class="section open" id="section-runtime">
            <div class="section-header" onclick="toggle('runtime')">
                <span class="section-title"><span>⚡</span> 런타임 환경</span>
                <div style="display:flex;align-items:center;gap:12px;">
                    <span class="badge {{RUNTIME_BADGE}}">{{RUNTIME_STATUS}}</span>
                    <span class="arrow">▼</span>
                </div>
            </div>
            <div class="section-content">
                <div class="runtime-grid">{{RUNTIME_CONTENT}}</div>
            </div>
        </div>
        
        {{FINDINGS_SECTIONS}}
        
        <footer>
            <a href="https://clawhub.ai/jooneyp/secucheck">secucheck</a> · 다시 실행: "보안 점검해줘"
        </footer>
    </div>
    
    <script>
        function toggle(id) {
            const section = document.getElementById('section-' + id);
            section.classList.toggle('open');
        }
        
        // Auto-collapse OK sections
        document.querySelectorAll('.section').forEach(s => {
            const badge = s.querySelector('.badge');
            if (badge && badge.classList.contains('badge-ok')) {
                s.classList.remove('open');
            }
        });
    </script>
</body>
</html>


path: dashboard/template-zh.html
content: <!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SecuCheck 安全审计</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&family=Noto+Sans+SC:wght@400;500&display=swap" rel="stylesheet">
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Noto Sans SC', 'Inter', sans-serif; background: #111; color: #ddd; font-size: 16px; line-height: 1.6; }
        .container { max-width: 960px; margin: 0 auto; padding: 40px 24px; }
        header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 32px; padding-bottom: 20px; border-bottom: 1px solid #333; }
        h1 { font-size: 20px; font-weight: 600; display: flex; align-items: center; gap: 10px; }
        .meta { color: #777; font-size: 14px; font-family: 'JetBrains Mono', monospace; }
        .summary { display: grid; grid-template-columns: 120px 1fr; gap: 24px; margin-bottom: 32px; }
        .score { background: #1a1a1a; border: 1px solid #333; padding: 24px; text-align: center; }
        .score-icon { font-size: 48px; }
        .score-label { font-size: 14px; font-weight: 500; margin-top: 8px; color: #999; }
        .counts { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; }
        .count-box { background: #1a1a1a; border: 1px solid #333; padding: 16px; text-align: center; }
        .count-num { font-size: 28px; font-weight: 600; font-family: 'JetBrains Mono', monospace; }
        .count-label { font-size: 12px; color: #777; margin-top: 4px; }
        .critical .count-num { color: #f66; }
        .high .count-num { color: #fa5; }
        .medium .count-num { color: #fc3; }
        .low .count-num { color: #6c6; }
        .info .count-num { color: #888; }
        .section { background: #1a1a1a; border: 1px solid #333; margin-bottom: 16px; }
        .section-header { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; cursor: pointer; user-select: none; }
        .section-header:hover { background: #222; }
        .section-title { font-size: 15px; font-weight: 500; display: flex; align-items: center; gap: 10px; }
        .badge { font-size: 12px; font-weight: 500; padding: 4px 10px; font-family: 'JetBrains Mono', monospace; }
        .badge-ok { color: #6c6; } .badge-low { color: #6c6; } .badge-medium { color: #fc3; } .badge-high { color: #fa5; } .badge-critical { color: #f66; }
        .section-content { display: none; padding: 20px; border-top: 1px solid #333; }
        .section.open .section-content { display: block; }
        .arrow { transition: transform 0.2s; color: #555; }
        .section.open .arrow { transform: rotate(180deg); }
        .runtime-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; }
        .runtime-card h4 { font-size: 12px; color: #666; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
        .runtime-item { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #2a2a2a; font-size: 14px; }
        .runtime-item:last-child { border-bottom: none; }
        .val { font-family: 'JetBrains Mono', monospace; font-size: 13px; }
        .val.good { color: #6c6; } .val.warn { color: #fc3; } .val.bad { color: #f66; }
        .findings-list { list-style: none; }
        .finding-item { padding: 14px 16px; margin-bottom: 10px; background: #111; border-left: 3px solid #333; font-size: 14px; }
        .finding-item.critical { border-left-color: #f66; } .finding-item.high { border-left-color: #fa5; } .finding-item.medium { border-left-color: #fc3; } .finding-item.low { border-left-color: #6c6; } .finding-item.info { border-left-color: #666; }
        .finding-item small { display: block; color: #888; margin-top: 6px; font-size: 13px; }
        footer { text-align: center; padding: 32px 0; color: #555; font-size: 13px; }
        footer a { color: #888; }
        @media (max-width: 700px) { .summary { grid-template-columns: 1fr; } .counts { grid-template-columns: repeat(3, 1fr); } }
    </style>
</head>
<body>
    <div class="container">
        <header><h1>🔒 SecuCheck</h1><div class="meta">{{SCAN_TIME}}<br>{{HOSTNAME}}</div></header>
        <div class="summary">
            <div class="score"><div class="score-icon">{{SCORE_EMOJI}}</div><div class="score-label">{{SCORE_LABEL}}</div></div>
            <div class="counts">
                <div class="count-box critical"><div class="count-num">{{COUNT_CRITICAL}}</div><div class="count-label">严重</div></div>
                <div class="count-box high"><div class="count-num">{{COUNT_HIGH}}</div><div class="count-label">高危</div></div>
                <div class="count-box medium"><div class="count-num">{{COUNT_MEDIUM}}</div><div class="count-label">中等</div></div>
                <div class="count-box low"><div class="count-num">{{COUNT_LOW}}</div><div class="count-label">低危</div></div>
                <div class="count-box info"><div class="count-num">{{COUNT_INFO}}</div><div class="count-label">信息</div></div>
            </div>
        </div>
        <div class="section open" id="section-runtime">
            <div class="section-header" onclick="toggle('runtime')">
                <span class="section-title"><span>⚡</span> 运行时环境</span>
                <div style="display:flex;align-items:center;gap:12px;"><span class="badge {{RUNTIME_BADGE}}">{{RUNTIME_STATUS}}</span><span class="arrow">▼</span></div>
            </div>
            <div class="section-content"><div class="runtime-grid">{{RUNTIME_CONTENT}}</div></div>
        </div>
        {{FINDINGS_SECTIONS}}
        <footer><a href="https://clawhub.ai/jooneyp/secucheck">secucheck</a> · 重新运行: "安全审计"</footer>
    </div>
    <script>
        function toggle(id) { document.getElementById('section-' + id).classList.toggle('open'); }
        document.querySelectorAll('.section').forEach(s => { if (s.querySelector('.badge-ok')) s.classList.remove('open'); });
    </script>
</body>
</html>


path: dashboard/template.html
content: <!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SecuCheck 보안 점검</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        
        body {
            font-family: 'Inter', -apple-system, sans-serif;
            background: #111;
            color: #ddd;
            font-size: 16px;
            line-height: 1.6;
        }
        
        .container { max-width: 960px; margin: 0 auto; padding: 40px 24px; }
        
        header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 32px;
            padding-bottom: 20px;
            border-bottom: 1px solid #333;
        }
        
        h1 { font-size: 20px; font-weight: 600; display: flex; align-items: center; gap: 10px; }
        .meta { color: #777; font-size: 14px; font-family: 'JetBrains Mono', monospace; }
        
        .summary {
            display: grid;
            grid-template-columns: 120px 1fr;
            gap: 24px;
            margin-bottom: 32px;
        }
        
        .score {
            background: #1a1a1a;
            border: 1px solid #333;
            padding: 24px;
            text-align: center;
        }
        
        .score-icon { font-size: 48px; }
        .score-label { font-size: 14px; font-weight: 500; margin-top: 8px; color: #999; }
        
        .counts {
            display: grid;
            grid-template-columns: repeat(5, 1fr);
            gap: 12px;
        }
        
        .count-box {
            background: #1a1a1a;
            border: 1px solid #333;
            padding: 16px;
            text-align: center;
        }
        
        .count-num {
            font-size: 28px;
            font-weight: 600;
            font-family: 'JetBrains Mono', monospace;
        }
        
        .count-label { font-size: 12px; color: #777; margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
        
        .critical .count-num { color: #f66; }
        .high .count-num { color: #fa5; }
        .medium .count-num { color: #fc3; }
        .low .count-num { color: #6c6; }
        .info .count-num { color: #888; }
        
        .section {
            background: #1a1a1a;
            border: 1px solid #333;
            margin-bottom: 16px;
        }
        
        .section-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 16px 20px;
            cursor: pointer;
            user-select: none;
        }
        
        .section-header:hover { background: #222; }
        
        .section-title {
            font-size: 15px;
            font-weight: 500;
            display: flex;
            align-items: center;
            gap: 10px;
        }
        
        .section-title span { font-size: 18px; }
        
        .badge {
            font-size: 12px;
            font-weight: 500;
            padding: 4px 10px;
            font-family: 'JetBrains Mono', monospace;
        }
        
        .badge-ok { color: #6c6; }
        .badge-low { color: #6c6; }
        .badge-medium { color: #fc3; }
        .badge-high { color: #fa5; }
        .badge-critical { color: #f66; }
        
        .section-content {
            display: none;
            padding: 20px;
            border-top: 1px solid #333;
        }
        
        .section.open .section-content { display: block; }
        
        .arrow {
            transition: transform 0.2s;
            color: #555;
        }
        
        .section.open .arrow { transform: rotate(180deg); }
        
        .runtime-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 20px;
        }
        
        .runtime-card h4 {
            font-size: 12px;
            color: #666;
            margin-bottom: 12px;
            text-transform: uppercase;
            letter-spacing: 0.5px;
            font-weight: 500;
        }
        
        .runtime-item {
            display: flex;
            justify-content: space-between;
            padding: 8px 0;
            border-bottom: 1px solid #2a2a2a;
            font-size: 14px;
        }
        
        .runtime-item:last-child { border-bottom: none; }
        
        .val {
            font-family: 'JetBrains Mono', monospace;
            font-size: 13px;
        }
        
        .val.good { color: #6c6; }
        .val.warn { color: #fc3; }
        .val.bad { color: #f66; }
        
        .findings-list { list-style: none; }
        
        .finding-item {
            padding: 14px 16px;
            margin-bottom: 10px;
            background: #111;
            border-left: 3px solid #333;
            font-size: 14px;
        }
        
        .finding-item:last-child { margin-bottom: 0; }
        
        .finding-item.critical { border-left-color: #f66; }
        .finding-item.high { border-left-color: #fa5; }
        .finding-item.medium { border-left-color: #fc3; }
        .finding-item.low { border-left-color: #6c6; }
        .finding-item.info { border-left-color: #666; }
        
        .finding-item small {
            display: block;
            color: #888;
            margin-top: 6px;
            font-size: 13px;
        }
        
        footer {
            text-align: center;
            padding: 32px 0;
            color: #555;
            font-size: 13px;
        }
        
        footer a { color: #888; text-decoration: none; }
        footer a:hover { color: #aaa; }
        
        @media (max-width: 700px) {
            .summary { grid-template-columns: 1fr; }
            .counts { grid-template-columns: repeat(3, 1fr); }
            .count-box:nth-child(4), .count-box:nth-child(5) { grid-column: span 1; }
        }
        
        @media (max-width: 500px) {
            .counts { grid-template-columns: repeat(2, 1fr); }
            .count-box:nth-child(5) { grid-column: span 2; }
        }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>🔒 SecuCheck</h1>
            <div class="meta">{{SCAN_TIME}}<br>{{HOSTNAME}}</div>
        </header>
        
        <div class="summary">
            <div class="score">
                <div class="score-icon">{{SCORE_EMOJI}}</div>
                <div class="score-label">{{SCORE_LABEL}}</div>
            </div>
            <div class="counts">
                <div class="count-box critical"><div class="count-num">{{COUNT_CRITICAL}}</div><div class="count-label">심각</div></div>
                <div class="count-box high"><div class="count-num">{{COUNT_HIGH}}</div><div class="count-label">높음</div></div>
                <div class="count-box medium"><div class="count-num">{{COUNT_MEDIUM}}</div><div class="count-label">중간</div></div>
                <div class="count-box low"><div class="count-num">{{COUNT_LOW}}</div><div class="count-label">낮음</div></div>
                <div class="count-box info"><div class="count-num">{{COUNT_INFO}}</div><div class="count-label">참고</div></div>
            </div>
        </div>
        
        <div class="section open" id="section-runtime">
            <div class="section-header" onclick="toggle('runtime')">
                <span class="section-title"><span>⚡</span> 런타임 환경</span>
                <div style="display:flex;align-items:center;gap:12px;">
                    <span class="badge {{RUNTIME_BADGE}}">{{RUNTIME_STATUS}}</span>
                    <span class="arrow">▼</span>
                </div>
            </div>
            <div class="section-content">
                <div class="runtime-grid">{{RUNTIME_CONTENT}}</div>
            </div>
        </div>
        
        {{FINDINGS_SECTIONS}}
        
        <footer>
            <a href="https://clawhub.ai/jooneyp/secucheck">secucheck</a> · 다시 실행: "보안 점검해줘"
        </footer>
    </div>
    
    <script>
        function toggle(id) {
            const section = document.getElementById('section-' + id);
            section.classList.toggle('open');
        }
        
        // Auto-collapse OK sections
        document.querySelectorAll('.section').forEach(s => {
            const badge = s.querySelector('.badge');
            if (badge && badge.classList.contains('badge-ok')) {
                s.classList.remove('open');
            }
        });
    </script>
</body>
</html>


path: scripts/gather_config.sh
content: #!/bin/bash
# Gather OpenClaw configuration for security analysis
# Outputs sanitized JSON (removes sensitive tokens)

CONFIG_FILE="${OPENCLAW_CONFIG:-$HOME/.openclaw/openclaw.json}"

if [ ! -f "$CONFIG_FILE" ]; then
    echo '{"error": "Config file not found", "path": "'"$CONFIG_FILE"'"}'
    exit 1
fi

# Read config and redact sensitive fields
cat "$CONFIG_FILE" | jq '
  # Redact tokens and passwords
  walk(
    if type == "object" then
      with_entries(
        if (.key | test("token|password|secret|apiKey|appToken|botToken"; "i")) then
          .value = "[REDACTED]"
        else
          .
        end
      )
    else
      .
    end
  )
'


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)