mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-17 07:21:21 +03:00
Add Picoclaw guardian + posture-review skills at v0.0.1 with wiki docs (#208)
* Add Picoclaw guardian + posture-review skills at v0.0.1 with wiki docs * fix(feed): add picoclaw to core platform taxonomy and filters * fix(picoclaw): resolve eslint errors in new skills * chore(nvd): include picoclaw in CVE polling and cleanup report --------- Co-authored-by: David Abutbul <David.a@prompt.security>
This commit is contained in:
@@ -399,6 +399,7 @@ jobs:
|
||||
(if ($blob | test("github\\.com/openclaw/openclaw|\\bopenclaw\\b|\\bclawdbot\\b|\\bmoltbot\\b")) then ["openclaw@*"] else [] end)
|
||||
+ (if ($blob | test("github\\.com/qwibitai/nanoclaw|\\bnanoclaw\\b|whatsapp-bot|\\bbaileys\\b")) then ["nanoclaw@*"] else [] end)
|
||||
+ (if ($blob | test("github\\.com/softwarepub/hermes|cpe:2\\.3:a:software-metadata\\.pub:hermes|\\bhermes workflow\\b|software publication with rich metadata")) then ["hermes@*"] else [] end)
|
||||
+ (if ($blob | test("github\\.com/[^/]+/picoclaw|\\bpicoclaw\\b|cpe:2\\.3:[aho]:[^:]*:picoclaw(?::|$)")) then ["picoclaw@*"] else [] end)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -414,14 +415,15 @@ jobs:
|
||||
[
|
||||
(if ($targets | map(strings | ascii_downcase | select(startswith("openclaw@") or test("^cpe:2\\.3:[aho]:openclaw:openclaw(?::|$)"))) | length > 0) then "openclaw" else empty end),
|
||||
(if ($targets | map(strings | ascii_downcase | select(startswith("nanoclaw@") or test("^cpe:2\\.3:[aho]:[^:]*:nanoclaw(?::|$)"))) | length > 0) then "nanoclaw" else empty end),
|
||||
(if ($targets | map(strings | ascii_downcase | select(startswith("hermes@") or test("^cpe:2\\.3:[aho]:software-metadata\\.pub:hermes(?::|$)"))) | length > 0) then "hermes" else empty end)
|
||||
(if ($targets | map(strings | ascii_downcase | select(startswith("hermes@") or test("^cpe:2\\.3:[aho]:software-metadata\\.pub:hermes(?::|$)"))) | length > 0) then "hermes" else empty end),
|
||||
(if ($targets | map(strings | ascii_downcase | select(startswith("picoclaw@") or test("^cpe:2\\.3:[aho]:[^:]*:picoclaw(?::|$)"))) | length > 0) then "picoclaw" else empty end)
|
||||
]
|
||||
);
|
||||
|
||||
def normalized_affected:
|
||||
(
|
||||
matched_targets
|
||||
| if length == 0 then ["openclaw@*", "nanoclaw@*", "hermes@*"] else . end
|
||||
| if length == 0 then ["openclaw@*", "nanoclaw@*", "hermes@*", "picoclaw@*"] else . end
|
||||
);
|
||||
|
||||
def normalized_platforms:
|
||||
@@ -432,7 +434,7 @@ jobs:
|
||||
else
|
||||
matched_targets as $targets
|
||||
| platforms_from_targets($targets) as $from_targets
|
||||
| if ($from_targets | length) > 0 then $from_targets else ["openclaw", "nanoclaw", "hermes"] end
|
||||
| if ($from_targets | length) > 0 then $from_targets else ["openclaw", "nanoclaw", "hermes", "picoclaw"] end
|
||||
end
|
||||
);
|
||||
|
||||
@@ -639,6 +641,7 @@ jobs:
|
||||
(if ($blob | test("github\\.com/openclaw/openclaw|\\bopenclaw\\b|\\bclawdbot\\b|\\bmoltbot\\b")) then ["openclaw@*"] else [] end)
|
||||
+ (if ($blob | test("github\\.com/qwibitai/nanoclaw|\\bnanoclaw\\b|whatsapp-bot|\\bbaileys\\b")) then ["nanoclaw@*"] else [] end)
|
||||
+ (if ($blob | test("github\\.com/softwarepub/hermes|cpe:2\\.3:a:software-metadata\\.pub:hermes|\\bhermes workflow\\b|software publication with rich metadata")) then ["hermes@*"] else [] end)
|
||||
+ (if ($blob | test("github\\.com/[^/]+/picoclaw|\\bpicoclaw\\b|cpe:2\\.3:[aho]:[^:]*:picoclaw(?::|$)")) then ["picoclaw@*"] else [] end)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -654,14 +657,15 @@ jobs:
|
||||
[
|
||||
(if ($targets | map(strings | ascii_downcase | select(startswith("openclaw@") or test("^cpe:2\\.3:[aho]:openclaw:openclaw(?::|$)"))) | length > 0) then "openclaw" else empty end),
|
||||
(if ($targets | map(strings | ascii_downcase | select(startswith("nanoclaw@") or test("^cpe:2\\.3:[aho]:[^:]*:nanoclaw(?::|$)"))) | length > 0) then "nanoclaw" else empty end),
|
||||
(if ($targets | map(strings | ascii_downcase | select(startswith("hermes@") or test("^cpe:2\\.3:[aho]:software-metadata\\.pub:hermes(?::|$)"))) | length > 0) then "hermes" else empty end)
|
||||
(if ($targets | map(strings | ascii_downcase | select(startswith("hermes@") or test("^cpe:2\\.3:[aho]:software-metadata\\.pub:hermes(?::|$)"))) | length > 0) then "hermes" else empty end),
|
||||
(if ($targets | map(strings | ascii_downcase | select(startswith("picoclaw@") or test("^cpe:2\\.3:[aho]:[^:]*:picoclaw(?::|$)"))) | length > 0) then "picoclaw" else empty end)
|
||||
]
|
||||
);
|
||||
|
||||
def normalized_affected:
|
||||
(
|
||||
matched_targets
|
||||
| if length == 0 then ["openclaw@*", "nanoclaw@*", "hermes@*"] else . end
|
||||
| if length == 0 then ["openclaw@*", "nanoclaw@*", "hermes@*", "picoclaw@*"] else . end
|
||||
);
|
||||
|
||||
def normalized_platforms:
|
||||
@@ -672,7 +676,7 @@ jobs:
|
||||
else
|
||||
matched_targets as $targets
|
||||
| platforms_from_targets($targets) as $from_targets
|
||||
| if ($from_targets | length) > 0 then $from_targets else ["openclaw", "nanoclaw", "hermes"] end
|
||||
| if ($from_targets | length) > 0 then $from_targets else ["openclaw", "nanoclaw", "hermes", "picoclaw"] end
|
||||
end
|
||||
);
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ ClawSec is a **complete security skill suite for AI agent platforms**. It provid
|
||||
- **OpenClaw** (MoltBot, Clawdbot, and clones) - Full suite with skill installer, file integrity protection, and security audits
|
||||
- **NanoClaw** - Containerized WhatsApp bot security with MCP tools for advisory monitoring, signature verification, and file integrity
|
||||
- **Hermes** - Hermes-native security skills for signed advisory feed verification, advisory-aware guarded verification, deterministic attestation generation, fail-closed verification, and baseline drift detection
|
||||
- **Picoclaw** - Lightweight AI gateway security posture checks with advisory awareness, config drift detection, release-artifact verification, and an optional separate self-pen-testing package
|
||||
|
||||
### Skill Feature Matrix
|
||||
|
||||
@@ -54,6 +55,8 @@ ClawSec is a **complete security skill suite for AI agent platforms**. It provid
|
||||
| clawtributor | OpenClaw | Yes | No | No | No |
|
||||
| hermes-attestation-guardian | Hermes | Yes (signed advisory feed verification) | Yes | No | Limited (advisory preflight gating only; no artifact signature/provenance install verification) |
|
||||
| openclaw-audit-watchdog | OpenClaw | No | No | Yes | No |
|
||||
| picoclaw-security-guardian | Picoclaw | Yes | Yes | No | Yes |
|
||||
| picoclaw-self-pen-testing | Picoclaw | No | No | Yes | No |
|
||||
| soul-guardian | OpenClaw | No | Yes | No | No |
|
||||
|
||||
### Core Capabilities
|
||||
@@ -135,12 +138,16 @@ Troubleshooting: if you see directories such as `~/.openclaw/workspace/$HOME/...
|
||||
Detailed platform and suite docs live in the wiki modules:
|
||||
- NanoClaw: [wiki/modules/nanoclaw-integration.md](wiki/modules/nanoclaw-integration.md)
|
||||
- Hermes: [wiki/modules/hermes-attestation-guardian.md](wiki/modules/hermes-attestation-guardian.md)
|
||||
- Picoclaw: [wiki/modules/picoclaw-security-guardian.md](wiki/modules/picoclaw-security-guardian.md)
|
||||
- Picoclaw self-pen-testing: [wiki/modules/picoclaw-self-pen-testing.md](wiki/modules/picoclaw-self-pen-testing.md)
|
||||
- ClawSec Suite (OpenClaw): [wiki/modules/clawsec-suite.md](wiki/modules/clawsec-suite.md)
|
||||
- CI/CD pipelines: [wiki/modules/automation-release.md](wiki/modules/automation-release.md)
|
||||
|
||||
Quick install links:
|
||||
- NanoClaw install: [skills/clawsec-nanoclaw/INSTALL.md](skills/clawsec-nanoclaw/INSTALL.md)
|
||||
- Hermes skill package: `skills/hermes-attestation-guardian/`
|
||||
- Picoclaw guardian package: `skills/picoclaw-security-guardian/`
|
||||
- Picoclaw self-pen-testing package: `skills/picoclaw-self-pen-testing/`
|
||||
- Suite package: `skills/clawsec-suite/`
|
||||
|
||||
---
|
||||
@@ -164,6 +171,7 @@ Compatibility mirror (legacy): `https://clawsec.prompt.security/releases/latest/
|
||||
The feed polls CVEs related to:
|
||||
- **OpenClaw Platform**: `OpenClaw`, `clawdbot`, `Moltbot`
|
||||
- **NanoClaw Platform**: `NanoClaw`, `WhatsApp-bot`, `baileys`
|
||||
- **Picoclaw Platform**: `Picoclaw`, `picoclaw`, lightweight AI gateways, MCP gateway exposure
|
||||
- Prompt injection patterns
|
||||
- Agent security vulnerabilities
|
||||
|
||||
@@ -219,7 +227,9 @@ This feature helps agents prioritize vulnerabilities that pose immediate threats
|
||||
**Platform values:**
|
||||
- `"openclaw"` - OpenClaw/Clawdbot/MoltBot only
|
||||
- `"nanoclaw"` - NanoClaw only
|
||||
- `["openclaw", "nanoclaw"]` - Both platforms
|
||||
- `"hermes"` - Hermes only
|
||||
- `"picoclaw"` - Picoclaw only
|
||||
- `["openclaw", "nanoclaw", "hermes", "picoclaw"]` - All core platforms
|
||||
- (empty/missing) - All platforms (backward compatible)
|
||||
|
||||
---
|
||||
@@ -340,6 +350,8 @@ npm run build
|
||||
│ ├── clawtributor/ # 🤝 Community reporting skill
|
||||
│ ├── hermes-attestation-guardian/ # 🛡️ Hermes attestation + drift verification
|
||||
│ ├── openclaw-audit-watchdog/ # 🔭 Automated audit skill
|
||||
│ ├── picoclaw-security-guardian/ # 🦐 Picoclaw posture/advisory/drift/supply-chain checks
|
||||
│ ├── picoclaw-self-pen-testing/ # 🧪 Picoclaw self-pen-testing checks (separate package)
|
||||
│ └── soul-guardian/ # 👻 File integrity skill
|
||||
├── utils/
|
||||
│ ├── package_skill.py # Skill packager utility
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ const PLATFORM_TABS = [
|
||||
{ value: 'openclaw', label: 'OpenClaw', active: 'bg-clawd-accent/20 text-clawd-accent border-2 border-clawd-accent', inactive: 'bg-clawd-800 text-gray-400 border border-clawd-700 hover:border-clawd-accent/50' },
|
||||
{ value: 'nanoclaw', label: 'NanoClaw', active: 'bg-clawd-secondary/20 text-clawd-secondary border-2 border-clawd-secondary', inactive: 'bg-clawd-800 text-gray-400 border border-clawd-700 hover:border-clawd-secondary/50' },
|
||||
{ value: 'hermes', label: 'Hermes', active: 'bg-emerald-500/20 text-emerald-300 border-2 border-emerald-400', inactive: 'bg-clawd-800 text-gray-400 border border-clawd-700 hover:border-emerald-400/50' },
|
||||
{ value: 'picoclaw', label: 'Picoclaw', active: 'bg-cyan-500/20 text-cyan-300 border-2 border-cyan-400', inactive: 'bg-clawd-800 text-gray-400 border border-clawd-700 hover:border-cyan-400/50' },
|
||||
{ value: 'other', label: 'Other', active: 'bg-clawd-600/40 text-gray-100 border-2 border-clawd-500', inactive: 'bg-clawd-800 text-gray-400 border border-clawd-700 hover:border-clawd-500/50' },
|
||||
] as const satisfies ReadonlyArray<FilterTabOption<AdvisoryPlatformFilter>>;
|
||||
|
||||
@@ -157,7 +158,7 @@ export const FeedSetup: React.FC = () => {
|
||||
<h1 className="text-3xl md:text-4xl text-white">Security Hardening Feed</h1>
|
||||
<p className="text-gray-400 max-w-2xl mx-auto">
|
||||
A continuous stream of security advisories from NVD CVE data and staff-approved community reports.
|
||||
This feed is automatically updated with OpenClaw, NanoClaw, and Hermes-related vulnerabilities and verified security incidents.
|
||||
This feed is automatically updated with OpenClaw, NanoClaw, Hermes, and Picoclaw-related vulnerabilities and verified security incidents.
|
||||
</p>
|
||||
{lastUpdated && (
|
||||
<p className="text-xs text-gray-500">
|
||||
|
||||
@@ -45,20 +45,22 @@ keyword|NanoClaw
|
||||
keyword|WhatsApp-bot
|
||||
keyword|baileys
|
||||
keyword|hermes workflow
|
||||
keyword|Picoclaw
|
||||
virtualMatchString|cpe:2.3:a:software-metadata.pub:hermes
|
||||
virtualMatchString|cpe:2.3:a:picoclaw:picoclaw
|
||||
EOF
|
||||
}
|
||||
|
||||
nvd_keyword_pattern() {
|
||||
echo 'OpenClaw|clawdbot|Moltbot|openclaw|NanoClaw|nanoclaw|WhatsApp-bot|baileys|HERMES workflow|software publication with rich metadata'
|
||||
echo 'OpenClaw|clawdbot|Moltbot|openclaw|NanoClaw|nanoclaw|WhatsApp-bot|baileys|HERMES workflow|software publication with rich metadata|Picoclaw|picoclaw'
|
||||
}
|
||||
|
||||
nvd_github_ref_pattern() {
|
||||
echo 'github\.com/openclaw/openclaw|github\.com/qwibitai/nanoclaw|github\.com/softwarepub/hermes'
|
||||
echo 'github\.com/openclaw/openclaw|github\.com/qwibitai/nanoclaw|github\.com/softwarepub/hermes|github\.com/[^/]+/picoclaw'
|
||||
}
|
||||
|
||||
nvd_cpe_pattern() {
|
||||
echo 'cpe:2\.3:a:software-metadata\.pub:hermes(?::|$)'
|
||||
echo 'cpe:2\.3:a:software-metadata\.pub:hermes(?::|$)|cpe:2\.3:[aho]:[^:]*:picoclaw(?::|$)'
|
||||
}
|
||||
|
||||
nvd_query_slug() {
|
||||
|
||||
@@ -271,6 +271,7 @@ jq --slurpfile existing "$TEMP_DIR/existing_ids.json" '
|
||||
(if ($blob | test("github\\.com/openclaw/openclaw|\\bopenclaw\\b|\\bclawdbot\\b|\\bmoltbot\\b")) then ["openclaw@*"] else [] end)
|
||||
+ (if ($blob | test("github\\.com/qwibitai/nanoclaw|\\bnanoclaw\\b|whatsapp-bot|\\bbaileys\\b")) then ["nanoclaw@*"] else [] end)
|
||||
+ (if ($blob | test("github\\.com/softwarepub/hermes|cpe:2\\.3:a:software-metadata\\.pub:hermes|\\bhermes workflow\\b|software publication with rich metadata")) then ["hermes@*"] else [] end)
|
||||
+ (if ($blob | test("github\\.com/[^/]+/picoclaw|\\bpicoclaw\\b|cpe:2\\.3:[aho]:[^:]*:picoclaw(?::|$)")) then ["picoclaw@*"] else [] end)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -286,14 +287,15 @@ jq --slurpfile existing "$TEMP_DIR/existing_ids.json" '
|
||||
[
|
||||
(if ($targets | map(strings | ascii_downcase | select(startswith("openclaw@") or test("^cpe:2\\.3:[aho]:openclaw:openclaw(?::|$)"))) | length > 0) then "openclaw" else empty end),
|
||||
(if ($targets | map(strings | ascii_downcase | select(startswith("nanoclaw@") or test("^cpe:2\\.3:[aho]:[^:]*:nanoclaw(?::|$)"))) | length > 0) then "nanoclaw" else empty end),
|
||||
(if ($targets | map(strings | ascii_downcase | select(startswith("hermes@") or test("^cpe:2\\.3:[aho]:software-metadata\\.pub:hermes(?::|$)"))) | length > 0) then "hermes" else empty end)
|
||||
(if ($targets | map(strings | ascii_downcase | select(startswith("hermes@") or test("^cpe:2\\.3:[aho]:software-metadata\\.pub:hermes(?::|$)"))) | length > 0) then "hermes" else empty end),
|
||||
(if ($targets | map(strings | ascii_downcase | select(startswith("picoclaw@") or test("^cpe:2\\.3:[aho]:[^:]*:picoclaw(?::|$)"))) | length > 0) then "picoclaw" else empty end)
|
||||
]
|
||||
);
|
||||
|
||||
def normalized_affected:
|
||||
(
|
||||
matched_targets
|
||||
| if length == 0 then ["openclaw@*", "nanoclaw@*", "hermes@*"] else . end
|
||||
| if length == 0 then ["openclaw@*", "nanoclaw@*", "hermes@*", "picoclaw@*"] else . end
|
||||
);
|
||||
|
||||
def normalized_platforms:
|
||||
@@ -304,7 +306,7 @@ jq --slurpfile existing "$TEMP_DIR/existing_ids.json" '
|
||||
else
|
||||
matched_targets as $targets
|
||||
| platforms_from_targets($targets) as $from_targets
|
||||
| if ($from_targets | length) > 0 then $from_targets else ["openclaw", "nanoclaw", "hermes"] end
|
||||
| if ($from_targets | length) > 0 then $from_targets else ["openclaw", "nanoclaw", "hermes", "picoclaw"] end
|
||||
end
|
||||
);
|
||||
|
||||
@@ -406,7 +408,7 @@ else
|
||||
jq -n --slurpfile advisories "$TEMP_DIR/new_advisories.json" --arg now "$NOW" '{
|
||||
version: "1.0.0",
|
||||
updated: $now,
|
||||
description: "Community-driven security advisory feed for ClawSec. Automatically updated with OpenClaw, NanoClaw, and Hermes-related CVEs from NVD.",
|
||||
description: "Community-driven security advisory feed for ClawSec. Automatically updated with OpenClaw, NanoClaw, Hermes, and Picoclaw-related CVEs from NVD.",
|
||||
advisories: (($advisories[0] // []) | sort_by(.published) | reverse)
|
||||
}' > "$TEMP_DIR/updated_feed.json"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## [0.0.1] - 2026-04-26
|
||||
|
||||
### Added
|
||||
- Initial Picoclaw-specific ClawSec skill package for advisory awareness, deterministic profile generation, drift detection, and supply-chain verification.
|
||||
- Picoclaw-native Docker pre-release install regression harness using `find_skills` / `install_skill` and skill-loader validation.
|
||||
|
||||
### Changed
|
||||
- Split optional posture-review checks into separate `picoclaw-self-pen-testing` package so this package remains the core public guardian lane.
|
||||
- Updated metadata/docs/regression expectations to keep this package focused on advisory, drift, and supply-chain checks.
|
||||
@@ -0,0 +1,51 @@
|
||||
# picoclaw-security-guardian
|
||||
|
||||
Picoclaw security posture skill for ClawSec.
|
||||
|
||||
Status: implemented (v0.0.1), Picoclaw-specific.
|
||||
|
||||
Detailed architecture/operator docs: `wiki/modules/picoclaw-security-guardian.md`.
|
||||
|
||||
## Support matrix mapping
|
||||
|
||||
| Skill name | supported platform | security feed | config drift | agent posture-review lane | chain of supply verification |
|
||||
|---|---|---|---|---|---|
|
||||
| picoclaw-security-guardian | Picoclaw | Yes | Yes | Separate package | Yes |
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Picoclaw-aware advisory filtering from a verified ClawSec feed/cache.
|
||||
- Deterministic local posture profile generation for configs, gateway exposure, tools, MCP, credentials/security files, and release artifacts.
|
||||
- Baseline drift comparison with critical/high/medium/low/info findings.
|
||||
- Supply-chain verification for release artifacts using SHA-256 manifests plus required Ed25519 detached signatures for passing provenance verdicts.
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
node scripts/generate_profile.mjs --output ~/.picoclaw/security/clawsec/current-profile.json
|
||||
node scripts/check_drift.mjs --baseline ~/.picoclaw/security/clawsec/baseline-profile.json --current ~/.picoclaw/security/clawsec/current-profile.json
|
||||
node scripts/verify_supply_chain.mjs --artifact ./picoclaw --checksums ./checksums.json --signature ./checksums.json.sig --public-key ./feed-signing-public.pem
|
||||
node scripts/check_advisories.mjs --feed ~/.picoclaw/security/clawsec/feed.json --state ~/.picoclaw/security/clawsec/feed-verification-state.json
|
||||
```
|
||||
|
||||
All scripts are read-only except profile/report outputs explicitly requested by `--output`.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
node test/profile.test.mjs
|
||||
node test/drift.test.mjs
|
||||
node test/supply_chain.test.mjs
|
||||
bash -n test/picoclaw_security_guardian_sandbox_regression.sh
|
||||
```
|
||||
|
||||
## Pre-release install regression
|
||||
|
||||
Run this before cutting v0.0.1 release artifacts:
|
||||
|
||||
```bash
|
||||
test/picoclaw_security_guardian_sandbox_regression.sh
|
||||
```
|
||||
|
||||
It uses Docker to publish the skill through a local ClawHub-compatible registry, installs it with Picoclaw's own `find_skills` / `install_skill` flow into an isolated Picoclaw workspace, confirms Picoclaw's skill loader can list/load it, then verifies the installed copy's profile, drift, advisory, and supply-chain paths.
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
name: picoclaw-security-guardian
|
||||
version: 0.0.1
|
||||
description: Picoclaw security posture skill with advisory awareness, configuration drift detection, and supply-chain verification guidance.
|
||||
homepage: https://clawsec.prompt.security
|
||||
author: prompt-security
|
||||
license: AGPL-3.0-or-later
|
||||
picoclaw:
|
||||
emoji: "🦐"
|
||||
category: "security"
|
||||
requires:
|
||||
bins: [node]
|
||||
test_requires:
|
||||
bins: [bash, docker, python3, node, openssl, zip]
|
||||
---
|
||||
|
||||
# Picoclaw Security Guardian
|
||||
|
||||
Detailed architecture/operator docs: `wiki/modules/picoclaw-security-guardian.md`.
|
||||
|
||||
## Goal
|
||||
|
||||
Provide Picoclaw with the same support-matrix security capabilities ClawSec tracks for mature platform modules:
|
||||
|
||||
| Skill name | supported platform | security feed | config drift | agent posture-review lane | chain of supply verification |
|
||||
|---|---|---|---|---|---|
|
||||
| picoclaw-security-guardian | Picoclaw | Yes | Yes | Separate package | Yes |
|
||||
|
||||
## Threat model
|
||||
|
||||
Picoclaw is a lightweight AI gateway that can expose chat channels, a Web UI, tool execution, MCP servers, credentials, schedulers, and embedded/router deployments. This skill focuses on the trust boundaries where those features become security-sensitive.
|
||||
|
||||
## Default safety posture
|
||||
|
||||
- Read-only by default.
|
||||
- No scheduler creation in v0.0.1.
|
||||
- No outbound network by default.
|
||||
- Writes only explicit report/profile outputs under `$PICOCLAW_HOME/security/clawsec/` unless the operator supplies test-local temporary paths.
|
||||
- Advisory checks fail closed when verification state is not verified unless the operator passes `--allow-unsigned` for a documented emergency/offline window.
|
||||
|
||||
## Security advisory awareness
|
||||
|
||||
Use `scripts/check_advisories.mjs` with a local feed/cache and verification state:
|
||||
|
||||
```bash
|
||||
node scripts/check_advisories.mjs --feed ~/.picoclaw/security/clawsec/feed.json --state ~/.picoclaw/security/clawsec/feed-verification-state.json
|
||||
```
|
||||
|
||||
The script filters advisories for `picoclaw`, `ai-gateway`, empty/all-platform advisories, or affected package entries containing `picoclaw`.
|
||||
|
||||
## Drift protection
|
||||
|
||||
Generate a deterministic profile:
|
||||
|
||||
```bash
|
||||
node scripts/generate_profile.mjs --output ~/.picoclaw/security/clawsec/current-profile.json
|
||||
```
|
||||
|
||||
Compare against an approved baseline:
|
||||
|
||||
```bash
|
||||
node scripts/check_drift.mjs --baseline ~/.picoclaw/security/clawsec/baseline-profile.json --current ~/.picoclaw/security/clawsec/current-profile.json --fail-on critical
|
||||
```
|
||||
|
||||
Critical drift includes public Web UI enablement, Web UI auth disablement, workspace restriction disablement, unsigned/insecure verification mode, verified-feed regression, and watched-file/release-artifact fingerprint changes.
|
||||
|
||||
## Chain-of-supply verification
|
||||
|
||||
Verify a Picoclaw release artifact against a checksum manifest plus detached signature. Signed manifest verification is required for a passing supply-chain verdict:
|
||||
|
||||
```bash
|
||||
node scripts/verify_supply_chain.mjs \
|
||||
--artifact ./picoclaw \
|
||||
--checksums ./checksums.json \
|
||||
--signature ./checksums.json.sig \
|
||||
--public-key ./feed-signing-public.pem
|
||||
```
|
||||
|
||||
Checksum-only mode is integrity-only, not provenance. Use `--allow-unsigned-checksums` only for short, documented offline triage windows; it should not satisfy production install verification.
|
||||
|
||||
## Operator review notes
|
||||
|
||||
- Treat public UI binding (`0.0.0.0`, `-public`) as a critical review item until auth and network allowlists are proven.
|
||||
- Treat MCP servers as separate trust boundaries; review each server's filesystem, network, and credential access.
|
||||
- Treat third-party OpenWrt/LuCI wrappers as separate supply-chain artifacts. Verify provenance before installing them on routers.
|
||||
- Never leave unsigned advisory mode enabled in recurring or production checks.
|
||||
|
||||
## Validation
|
||||
|
||||
```bash
|
||||
python utils/validate_skill.py skills/picoclaw-security-guardian
|
||||
node skills/picoclaw-security-guardian/test/profile.test.mjs
|
||||
node skills/picoclaw-security-guardian/test/drift.test.mjs
|
||||
node skills/picoclaw-security-guardian/test/supply_chain.test.mjs
|
||||
bash -n skills/picoclaw-security-guardian/test/picoclaw_security_guardian_sandbox_regression.sh
|
||||
```
|
||||
|
||||
## Pre-release install regression
|
||||
|
||||
Before publishing v0.0.1 release artifacts, run the isolated install lane from the repo root:
|
||||
|
||||
```bash
|
||||
skills/picoclaw-security-guardian/test/picoclaw_security_guardian_sandbox_regression.sh
|
||||
```
|
||||
|
||||
The regression installs the skill through Picoclaw's own `find_skills` / `install_skill` path from a local ClawHub-compatible registry into an isolated Docker-hosted Picoclaw workspace with isolated `HOME`, `PICOCLAW_HOME`, and `PICOCLAW_WORKSPACE`. It verifies signed release-artifact preflight inputs, confirms Picoclaw's skill loader can list/load the installed skill, then runs the installed copy's profile, drift, advisory fail-closed, advisory filtering, and supply-chain verification paths against Picoclaw-style `config.json` and `launcher-config.json` files.
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import fs from "node:fs";
|
||||
|
||||
export function loadAdvisoryFeed(feedPath) { return JSON.parse(fs.readFileSync(feedPath, "utf8")); }
|
||||
export function loadFeedState(statePath) { if (!statePath || !fs.existsSync(statePath)) return { status: "unknown" }; return JSON.parse(fs.readFileSync(statePath, "utf8")); }
|
||||
export function isPicoclawAdvisory(advisory) {
|
||||
const platforms = Array.isArray(advisory?.platforms) ? advisory.platforms.map(x=>String(x).toLowerCase()) : [];
|
||||
const affected = Array.isArray(advisory?.affected) ? advisory.affected.map(x=>String(x).toLowerCase()) : [];
|
||||
const blob = `${advisory?.title || ""} ${advisory?.description || ""} ${advisory?.type || ""}`.toLowerCase();
|
||||
return platforms.length === 0 || platforms.includes("picoclaw") || platforms.includes("ai-gateway") || affected.some(x=>x.includes("picoclaw")) || blob.includes("picoclaw");
|
||||
}
|
||||
export function checkPicoclawAdvisories({ feedPath, statePath, allowUnsigned = false }) {
|
||||
const state = loadFeedState(statePath);
|
||||
if (!allowUnsigned && state.status !== "verified") throw new Error(`advisory feed state is not verified: ${state.status || "missing"}`);
|
||||
const feed = loadAdvisoryFeed(feedPath);
|
||||
const advisories = (feed.advisories || []).filter(isPicoclawAdvisory);
|
||||
return { status: "ok", feed_version: feed.version || null, verified_state: state.status || "unknown", count: advisories.length, advisories };
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
const SEVERITY_ORDER = ["critical", "high", "medium", "low", "info"];
|
||||
|
||||
function bump(summary, sev) { summary[sev] = (summary[sev] || 0) + 1; }
|
||||
function bool(value) { return !!value; }
|
||||
function add(findings, summary, severity, code, path, message, details = undefined) {
|
||||
const finding = { severity, code, path, message };
|
||||
if (details) finding.details = details;
|
||||
findings.push(finding); bump(summary, severity);
|
||||
}
|
||||
function byPath(entries) { const m = new Map(); for (const e of Array.isArray(entries) ? entries : []) if (e?.path) m.set(e.path, e); return m; }
|
||||
function compareBool({ before, after, path, codeOnEnable, codeOnDisable, enableSeverity, findings, summary }) {
|
||||
if (bool(before) === bool(after)) return;
|
||||
if (!before && after) add(findings, summary, enableSeverity, codeOnEnable, path, `${path} changed false -> true`);
|
||||
else add(findings, summary, "info", codeOnDisable, path, `${path} changed true -> false`);
|
||||
}
|
||||
function compareHashSet(beforeEntries, afterEntries, changedCode, removedCode, findings, summary) {
|
||||
const b = byPath(beforeEntries); const a = byPath(afterEntries);
|
||||
for (const [p, before] of b.entries()) {
|
||||
const after = a.get(p);
|
||||
if (!after) { add(findings, summary, "high", removedCode, p, `${p} missing from current profile`); continue; }
|
||||
if ((before.sha256 || null) !== (after.sha256 || null)) add(findings, summary, "critical", changedCode, p, `${p} fingerprint changed`);
|
||||
}
|
||||
for (const [p] of a.entries()) if (!b.has(p)) add(findings, summary, "low", "NEW_INTEGRITY_SCOPE", p, `${p} added to integrity tracking scope`);
|
||||
}
|
||||
export function diffPicoclawProfiles(baseline, current) {
|
||||
const findings=[]; const summary={critical:0, high:0, medium:0, low:0, info:0};
|
||||
const b=baseline||{}; const c=current||{};
|
||||
if (b.platform !== c.platform) add(findings, summary, "critical", "PLATFORM_MISMATCH", "platform", `platform changed ${b.platform} -> ${c.platform}`);
|
||||
if (b.schema_version !== c.schema_version) add(findings, summary, "high", "SCHEMA_VERSION_CHANGED", "schema_version", `schema_version changed ${b.schema_version} -> ${c.schema_version}`);
|
||||
const br=b.posture?.runtime||{}; const cr=c.posture?.runtime||{};
|
||||
compareBool({before: br.ui?.public_web_ui, after: cr.ui?.public_web_ui, path:"posture.runtime.ui.public_web_ui", codeOnEnable:"PUBLIC_WEB_UI_ENABLED", codeOnDisable:"PUBLIC_WEB_UI_DISABLED", enableSeverity:"critical", findings, summary});
|
||||
compareBool({before: br.ui?.auth_disabled, after: cr.ui?.auth_disabled, path:"posture.runtime.ui.auth_disabled", codeOnEnable:"WEB_UI_AUTH_DISABLED", codeOnDisable:"WEB_UI_AUTH_REENABLED", enableSeverity:"critical", findings, summary});
|
||||
compareBool({before: br.tools?.unrestricted_workspace, after: cr.tools?.unrestricted_workspace, path:"posture.runtime.tools.unrestricted_workspace", codeOnEnable:"WORKSPACE_RESTRICTION_DISABLED", codeOnDisable:"WORKSPACE_RESTRICTION_RESTORED", enableSeverity:"critical", findings, summary});
|
||||
compareBool({before: br.risky_toggles?.allow_unsigned_mode, after: cr.risky_toggles?.allow_unsigned_mode, path:"posture.runtime.risky_toggles.allow_unsigned_mode", codeOnEnable:"UNSIGNED_MODE_ENABLED", codeOnDisable:"UNSIGNED_MODE_DISABLED", enableSeverity:"critical", findings, summary});
|
||||
compareBool({before: br.mcp?.enabled, after: cr.mcp?.enabled, path:"posture.runtime.mcp.enabled", codeOnEnable:"MCP_ENABLED", codeOnDisable:"MCP_DISABLED", enableSeverity:"high", findings, summary});
|
||||
compareBool({before: br.scheduler?.enabled, after: cr.scheduler?.enabled, path:"posture.runtime.scheduler.enabled", codeOnEnable:"SCHEDULER_ENABLED", codeOnDisable:"SCHEDULER_DISABLED", enableSeverity:"medium", findings, summary});
|
||||
if ((br.secrets?.config_secret_markers||0) < (cr.secrets?.config_secret_markers||0)) add(findings, summary, "high", "SECRET_MARKERS_INCREASED", "posture.runtime.secrets.config_secret_markers", "config secret markers increased", { before: br.secrets?.config_secret_markers||0, after: cr.secrets?.config_secret_markers||0 });
|
||||
if (b.posture?.feed_verification?.status === "verified" && c.posture?.feed_verification?.status !== "verified") add(findings, summary, "critical", "FEED_VERIFICATION_REGRESSION", "posture.feed_verification.status", `Feed verification regressed verified -> ${c.posture?.feed_verification?.status || "unknown"}`);
|
||||
compareHashSet(b.posture?.integrity?.watched_files, c.posture?.integrity?.watched_files, "WATCHED_FILE_DRIFT", "WATCHED_FILE_REMOVED", findings, summary);
|
||||
compareHashSet(b.posture?.integrity?.release_artifacts, c.posture?.integrity?.release_artifacts, "RELEASE_ARTIFACT_DRIFT", "RELEASE_ARTIFACT_REMOVED", findings, summary);
|
||||
findings.sort((x,y)=>SEVERITY_ORDER.indexOf(x.severity)-SEVERITY_ORDER.indexOf(y.severity)||String(x.code).localeCompare(String(y.code))||String(x.path).localeCompare(String(y.path)));
|
||||
return { summary, findings };
|
||||
}
|
||||
export function highestSeverity(findings=[]) { return SEVERITY_ORDER.find(s => findings.some(f => f?.severity===s)) || null; }
|
||||
export function severityAtOrAbove(severity, threshold) { if (!threshold || threshold === "none") return false; const a=SEVERITY_ORDER.indexOf(severity), b=SEVERITY_ORDER.indexOf(threshold); return a >= 0 && b >= 0 && a <= b; }
|
||||
@@ -0,0 +1,270 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
export const SCHEMA_VERSION = "picoclaw-profile/v1";
|
||||
export const PROFILE_VERSION = "0.0.1";
|
||||
|
||||
export function stableStringify(value, space = 2) {
|
||||
return JSON.stringify(sortDeep(value), null, space);
|
||||
}
|
||||
|
||||
function sortDeep(value) {
|
||||
if (Array.isArray(value)) return value.map(sortDeep);
|
||||
if (!value || typeof value !== "object") return value;
|
||||
const out = {};
|
||||
for (const key of Object.keys(value).sort()) out[key] = sortDeep(value[key]);
|
||||
return out;
|
||||
}
|
||||
|
||||
export function sha256Hex(content) {
|
||||
return crypto.createHash("sha256").update(content).digest("hex");
|
||||
}
|
||||
|
||||
export function sha256FileHex(filePath) {
|
||||
return sha256Hex(fs.readFileSync(filePath));
|
||||
}
|
||||
|
||||
export function defaultPicoclawHome() {
|
||||
return path.resolve(process.env.PICOCLAW_HOME || path.join(os.homedir(), ".picoclaw"));
|
||||
}
|
||||
|
||||
export function defaultOutputPath(picoclawHome = defaultPicoclawHome()) {
|
||||
return path.join(picoclawHome, "security", "clawsec", "current-profile.json");
|
||||
}
|
||||
|
||||
export function expandUserPath(raw, base = defaultPicoclawHome()) {
|
||||
if (!raw) return "";
|
||||
const value = String(raw).trim();
|
||||
if (!value) return "";
|
||||
if (value === "~") return os.homedir();
|
||||
if (value.startsWith("~/")) return path.join(os.homedir(), value.slice(2));
|
||||
if (value.startsWith("$PICOCLAW_HOME/")) return path.join(base, value.slice("$PICOCLAW_HOME/".length));
|
||||
return path.resolve(value);
|
||||
}
|
||||
|
||||
export function isPathInside(childPath, parentPath) {
|
||||
const child = path.resolve(childPath);
|
||||
const parent = path.resolve(parentPath);
|
||||
const rel = path.relative(parent, child);
|
||||
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
||||
}
|
||||
|
||||
function nearestExistingAncestor(candidatePath) {
|
||||
let candidate = path.resolve(candidatePath);
|
||||
while (!fs.existsSync(candidate)) {
|
||||
const parent = path.dirname(candidate);
|
||||
if (parent === candidate) return candidate;
|
||||
candidate = parent;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function realpathWithMissingTail(candidatePath) {
|
||||
const resolved = path.resolve(candidatePath);
|
||||
const ancestor = nearestExistingAncestor(resolved);
|
||||
const realAncestor = fs.realpathSync.native ? fs.realpathSync.native(ancestor) : fs.realpathSync(ancestor);
|
||||
const rel = path.relative(ancestor, resolved);
|
||||
return rel ? path.join(realAncestor, rel) : realAncestor;
|
||||
}
|
||||
|
||||
export function confineOutputToPicoclawHome(candidatePath, picoclawHome = defaultPicoclawHome()) {
|
||||
const root = path.resolve(picoclawHome);
|
||||
const resolved = path.resolve(candidatePath);
|
||||
if (!isPathInside(resolved, root)) throw new Error(`output path must stay under ${root}`);
|
||||
const rootReal = realpathWithMissingTail(root);
|
||||
const resolvedReal = realpathWithMissingTail(resolved);
|
||||
if (!isPathInside(resolvedReal, rootReal)) throw new Error(`output path must stay under ${rootReal}`);
|
||||
if (fs.existsSync(resolved) && fs.lstatSync(resolved).isSymbolicLink()) {
|
||||
throw new Error(`output path must not be a symlink: ${resolved}`);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export function parseJsonFile(filePath) {
|
||||
if (!filePath || !fs.existsSync(filePath)) return null;
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
}
|
||||
|
||||
export function detectConfigPaths(picoclawHome = defaultPicoclawHome(), extraConfig = null) {
|
||||
const candidates = [
|
||||
process.env.PICOCLAW_CONFIG,
|
||||
extraConfig,
|
||||
path.join(picoclawHome, "config.yaml"),
|
||||
path.join(picoclawHome, "config.yml"),
|
||||
path.join(picoclawHome, "config.json"),
|
||||
path.join(picoclawHome, "launcher-config.json"),
|
||||
path.join(picoclawHome, ".security.yml"),
|
||||
path.join(picoclawHome, "security.yml"),
|
||||
].filter(Boolean).map((p) => expandUserPath(p, picoclawHome));
|
||||
return [...new Set(candidates)];
|
||||
}
|
||||
|
||||
function safeReadText(filePath, maxBytes = 1024 * 1024) {
|
||||
try {
|
||||
const st = fs.statSync(filePath);
|
||||
if (!st.isFile() || st.size > maxBytes) return "";
|
||||
return fs.readFileSync(filePath, "utf8");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function fingerprintPath(filePath) {
|
||||
const exists = fs.existsSync(filePath);
|
||||
if (!exists) return { path: filePath, exists: false };
|
||||
const st = fs.statSync(filePath);
|
||||
return {
|
||||
path: filePath,
|
||||
exists: true,
|
||||
type: st.isDirectory() ? "directory" : st.isFile() ? "file" : "other",
|
||||
size: st.isFile() ? st.size : null,
|
||||
mode: (st.mode & 0o777).toString(8).padStart(3, "0"),
|
||||
sha256: st.isFile() ? sha256FileHex(filePath) : null,
|
||||
};
|
||||
}
|
||||
|
||||
function truthyFromText(text, patterns) {
|
||||
const low = text.toLowerCase();
|
||||
return patterns.some((p) => low.includes(p));
|
||||
}
|
||||
|
||||
function truthyRegex(text, patterns) {
|
||||
return patterns.some((p) => p.test(text));
|
||||
}
|
||||
|
||||
function jsonBoolPattern(key, expected) {
|
||||
return new RegExp(`"${key}"\\s*:\\s*${expected ? "true" : "false"}`, "i");
|
||||
}
|
||||
|
||||
function jsonEmptyStringPattern(key) {
|
||||
return new RegExp(`"${key}"\\s*:\\s*"\\s*"`, "i");
|
||||
}
|
||||
|
||||
function jsonStringPattern(key, value) {
|
||||
return new RegExp(`"${key}"\\s*:\\s*"${value.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}"`, "i");
|
||||
}
|
||||
|
||||
function analyzeConfigText(text) {
|
||||
return {
|
||||
public_web_ui: truthyFromText(text, [
|
||||
"public: true",
|
||||
"bind: 0.0.0.0",
|
||||
"host: 0.0.0.0",
|
||||
"-public",
|
||||
'"public": true',
|
||||
'"bind": "0.0.0.0"',
|
||||
'"host": "0.0.0.0"',
|
||||
'"listen": "0.0.0.0"',
|
||||
]) || truthyRegex(text, [
|
||||
jsonBoolPattern("public", true),
|
||||
jsonStringPattern("bind", "0.0.0.0"),
|
||||
jsonStringPattern("host", "0.0.0.0"),
|
||||
jsonStringPattern("listen", "0.0.0.0"),
|
||||
]),
|
||||
auth_disabled: truthyFromText(text, [
|
||||
"auth: false",
|
||||
"disable_auth: true",
|
||||
"no_auth: true",
|
||||
"password: ''",
|
||||
'password: ""',
|
||||
'"auth": false',
|
||||
'"disable_auth": true',
|
||||
'"no_auth": true',
|
||||
'"require_auth": false',
|
||||
'"dashboard_auth": false',
|
||||
'"password": ""',
|
||||
'"dashboard_password_hash": ""',
|
||||
'"launcher_token": ""',
|
||||
]) || truthyRegex(text, [
|
||||
jsonBoolPattern("auth", false),
|
||||
jsonBoolPattern("disable_auth", true),
|
||||
jsonBoolPattern("no_auth", true),
|
||||
jsonBoolPattern("require_auth", false),
|
||||
jsonBoolPattern("dashboard_auth", false),
|
||||
jsonEmptyStringPattern("password"),
|
||||
jsonEmptyStringPattern("dashboard_password_hash"),
|
||||
jsonEmptyStringPattern("launcher_token"),
|
||||
]),
|
||||
allow_unsigned: truthyFromText(text, [
|
||||
"allow_unsigned",
|
||||
"skip_signature",
|
||||
"disable_signature",
|
||||
"insecure_skip_verify",
|
||||
]),
|
||||
unrestricted_workspace: truthyFromText(text, [
|
||||
"restrict_to_workspace: false",
|
||||
"workspace_restriction: false",
|
||||
"sandbox: false",
|
||||
'"restrict_to_workspace": false',
|
||||
'"workspace_restriction": false',
|
||||
'"sandbox": false',
|
||||
]) || truthyRegex(text, [
|
||||
jsonBoolPattern("restrict_to_workspace", false),
|
||||
jsonBoolPattern("workspace_restriction", false),
|
||||
jsonBoolPattern("sandbox", false),
|
||||
]),
|
||||
mcp_enabled: truthyFromText(text, ["mcp:", "mcp_servers", "modelcontextprotocol", '"mcp"', '"mcp_servers"']),
|
||||
tools_enabled: truthyFromText(text, ["tools:", "code_execution", "shell", "filesystem", '"tools"', '"exec"', '"shell"']),
|
||||
scheduler_enabled: truthyFromText(text, ["cron", "schedule", "scheduler"]),
|
||||
secret_markers: (text.match(/(api[_-]?key|token|secret|password)\s*[":=]+\s*['"]?[^\s'"]{8,}/gi) || []).length,
|
||||
};
|
||||
}
|
||||
|
||||
function mergeConfigSignals(paths) {
|
||||
const signals = {
|
||||
public_web_ui: false,
|
||||
auth_disabled: false,
|
||||
allow_unsigned: false,
|
||||
unrestricted_workspace: false,
|
||||
mcp_enabled: false,
|
||||
tools_enabled: false,
|
||||
scheduler_enabled: false,
|
||||
secret_markers: 0,
|
||||
};
|
||||
for (const p of paths) {
|
||||
const text = safeReadText(p);
|
||||
const found = analyzeConfigText(text);
|
||||
for (const [k, v] of Object.entries(found)) {
|
||||
if (typeof v === "boolean") signals[k] = signals[k] || v;
|
||||
else signals[k] += v;
|
||||
}
|
||||
}
|
||||
return signals;
|
||||
}
|
||||
|
||||
export function buildPicoclawProfile(options = {}) {
|
||||
const picoclawHome = path.resolve(options.picoclawHome || defaultPicoclawHome());
|
||||
const generatedAt = options.generatedAt || new Date().toISOString();
|
||||
const configPaths = detectConfigPaths(picoclawHome, options.configPath);
|
||||
const watchedFiles = [...new Set([...(options.watchFiles || []), ...configPaths].filter(Boolean).map((p) => expandUserPath(p, picoclawHome)))];
|
||||
const releaseArtifacts = [...new Set((options.releaseArtifacts || []).filter(Boolean).map((p) => expandUserPath(p, picoclawHome)))];
|
||||
const signals = options.signals || mergeConfigSignals(watchedFiles);
|
||||
const profile = {
|
||||
schema_version: SCHEMA_VERSION,
|
||||
platform: "picoclaw",
|
||||
generated_at: generatedAt,
|
||||
generator: { name: "picoclaw-security-guardian", version: PROFILE_VERSION },
|
||||
posture: {
|
||||
runtime: {
|
||||
home: picoclawHome,
|
||||
config_paths: configPaths,
|
||||
gateways: options.gateways || {},
|
||||
ui: { public_web_ui: !!signals.public_web_ui, auth_disabled: !!signals.auth_disabled },
|
||||
tools: { enabled: !!signals.tools_enabled, unrestricted_workspace: !!signals.unrestricted_workspace },
|
||||
mcp: { enabled: !!signals.mcp_enabled },
|
||||
scheduler: { enabled: !!signals.scheduler_enabled },
|
||||
risky_toggles: { allow_unsigned_mode: !!signals.allow_unsigned },
|
||||
secrets: { config_secret_markers: signals.secret_markers || 0 },
|
||||
},
|
||||
integrity: {
|
||||
watched_files: watchedFiles.map(fingerprintPath),
|
||||
release_artifacts: releaseArtifacts.map(fingerprintPath),
|
||||
},
|
||||
feed_verification: options.feedVerification || { status: "unknown" },
|
||||
},
|
||||
};
|
||||
profile.digests = { canonical_sha256: sha256Hex(stableStringify({ ...profile, digests: undefined }, 0)) };
|
||||
return profile;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { sha256FileHex } from "./profile.mjs";
|
||||
|
||||
function normalizeManifestPath(value) {
|
||||
return String(value || "").trim().replace(/^\.\//, "");
|
||||
}
|
||||
|
||||
function parseChecksums(raw) {
|
||||
const text = String(raw || "");
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) throw new Error("checksum manifest is empty");
|
||||
|
||||
if (trimmed.startsWith("{")) {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
const source = parsed.files && typeof parsed.files === "object" ? parsed.files : parsed;
|
||||
const out = {};
|
||||
for (const [manifestPath, entry] of Object.entries(source)) {
|
||||
const normalized = normalizeManifestPath(manifestPath);
|
||||
const hash = typeof entry === "string" ? entry : entry?.sha256;
|
||||
if (typeof hash === "string" && /^[a-fA-F0-9]{64}$/.test(hash.trim())) {
|
||||
if (out[normalized]) throw new Error(`duplicate checksum entry: ${normalized}`);
|
||||
out[normalized] = hash.trim().toLowerCase();
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const out = {};
|
||||
const basenameCounts = new Map();
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
const m = line.match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/);
|
||||
if (!m) continue;
|
||||
const manifestPath = normalizeManifestPath(m[2]);
|
||||
if (out[manifestPath]) throw new Error(`duplicate checksum entry: ${manifestPath}`);
|
||||
out[manifestPath] = m[1].toLowerCase();
|
||||
const base = path.basename(manifestPath);
|
||||
basenameCounts.set(base, (basenameCounts.get(base) || 0) + 1);
|
||||
}
|
||||
for (const [base, count] of basenameCounts.entries()) {
|
||||
if (count > 1) throw new Error(`ambiguous duplicate checksum basename: ${base}`);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function expectedForArtifact(files, artifactPath, manifestName = null) {
|
||||
const candidates = [manifestName, artifactPath, path.basename(artifactPath)]
|
||||
.filter(Boolean)
|
||||
.map(normalizeManifestPath);
|
||||
for (const candidate of candidates) {
|
||||
if (files[candidate]) return files[candidate];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function verifyChecksums({ artifactPath, checksumsPath, manifestName = null }) {
|
||||
const files = parseChecksums(fs.readFileSync(checksumsPath, "utf8"));
|
||||
const expected = expectedForArtifact(files, artifactPath, manifestName);
|
||||
if (!expected) {
|
||||
return { ok: false, status: "missing", artifact: artifactPath, message: "artifact not present in checksum manifest" };
|
||||
}
|
||||
const actual = sha256FileHex(artifactPath);
|
||||
return { ok: actual === expected, status: actual === expected ? "verified" : "mismatch", artifact: artifactPath, expected, actual };
|
||||
}
|
||||
|
||||
export function verifyDetachedSignature({ manifestPath, signaturePath, publicKeyPath }) {
|
||||
const manifestBytes = fs.readFileSync(manifestPath);
|
||||
const signatureText = fs.readFileSync(signaturePath, "utf8").trim();
|
||||
const sig = Buffer.from(signatureText.replace(/\s+/g, ""), "base64");
|
||||
const key = crypto.createPublicKey(fs.readFileSync(publicKeyPath, "utf8"));
|
||||
const ok = crypto.verify(null, manifestBytes, key, sig);
|
||||
return { ok, status: ok ? "verified" : "mismatch", manifest: manifestPath, signature: signaturePath };
|
||||
}
|
||||
|
||||
export function verifySupplyChain(options) {
|
||||
const checksum = verifyChecksums(options);
|
||||
if (!options.allowUnsignedChecksums && (!options.signaturePath || !options.publicKeyPath)) {
|
||||
return {
|
||||
checksum,
|
||||
signature: { ok: false, status: "missing" },
|
||||
ok: false,
|
||||
message: "detached signature and trusted public key are required for supply-chain verification",
|
||||
};
|
||||
}
|
||||
const result = { checksum, signature: { ok: null, status: "not_checked" }, ok: checksum.ok };
|
||||
if (options.signaturePath && options.publicKeyPath) {
|
||||
result.signature = verifyDetachedSignature({
|
||||
manifestPath: options.checksumsPath,
|
||||
signaturePath: options.signaturePath,
|
||||
publicKeyPath: options.publicKeyPath,
|
||||
});
|
||||
result.ok = checksum.ok && result.signature.ok;
|
||||
} else {
|
||||
result.signature = { ok: null, status: "unsigned_checksum_only" };
|
||||
result.ok = checksum.ok;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
import { checkPicoclawAdvisories } from "../lib/advisories.mjs"; import { stableStringify } from "../lib/profile.mjs";
|
||||
function parse(argv){const a={allowUnsigned:false}; for(let i=0;i<argv.length;i++){const t=argv[i]; if(t==="--feed") a.feedPath=argv[++i]; else if(t==="--state") a.statePath=argv[++i]; else if(t==="--allow-unsigned") a.allowUnsigned=true; else throw new Error(`Unknown argument: ${t}`);} if(!a.feedPath) throw new Error("--feed is required"); return a;}
|
||||
const result=checkPicoclawAdvisories(parse(process.argv.slice(2))); console.log(stableStringify(result));
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs"; import { diffPicoclawProfiles, highestSeverity, severityAtOrAbove } from "../lib/drift.mjs"; import { stableStringify } from "../lib/profile.mjs";
|
||||
function parse(argv){const a={failOn:"critical"}; for(let i=0;i<argv.length;i++){const t=argv[i]; if(t==="--baseline") a.baseline=argv[++i]; else if(t==="--current") a.current=argv[++i]; else if(t==="--fail-on") a.failOn=argv[++i]; else throw new Error(`Unknown argument: ${t}`);} if(!a.baseline||!a.current) throw new Error("--baseline and --current are required"); return a;}
|
||||
const a=parse(process.argv.slice(2)); const result=diffPicoclawProfiles(JSON.parse(fs.readFileSync(a.baseline,"utf8")), JSON.parse(fs.readFileSync(a.current,"utf8"))); console.log(stableStringify(result)); const hi=highestSeverity(result.findings); if(severityAtOrAbove(hi,a.failOn)) process.exit(2);
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { buildPicoclawProfile, confineOutputToPicoclawHome, defaultOutputPath, defaultPicoclawHome, stableStringify } from "../lib/profile.mjs";
|
||||
|
||||
function parse(argv) {
|
||||
const args = { watch: [], artifact: [], output: null, home: defaultPicoclawHome(), generatedAt: null, config: null };
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const token = argv[i];
|
||||
if (token === "--output") args.output = argv[++i];
|
||||
else if (token === "--home") args.home = argv[++i];
|
||||
else if (token === "--watch") args.watch.push(argv[++i]);
|
||||
else if (token === "--artifact") args.artifact.push(argv[++i]);
|
||||
else if (token === "--generated-at") args.generatedAt = argv[++i];
|
||||
else if (token === "--config") args.config = argv[++i];
|
||||
else if (token === "--help") {
|
||||
console.log("Usage: node scripts/generate_profile.mjs [--output path] [--home path] [--config path] [--watch path] [--artifact path]");
|
||||
process.exit(0);
|
||||
} else {
|
||||
throw new Error(`Unknown argument: ${token}`);
|
||||
}
|
||||
}
|
||||
if (!args.output) args.output = defaultOutputPath(args.home);
|
||||
return args;
|
||||
}
|
||||
|
||||
function writeNoFollow(outPath, body) {
|
||||
const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC | (fs.constants.O_NOFOLLOW || 0);
|
||||
const fd = fs.openSync(outPath, flags, 0o600);
|
||||
try {
|
||||
fs.writeFileSync(fd, body, "utf8");
|
||||
fs.fsyncSync(fd);
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
}
|
||||
|
||||
const args = parse(process.argv.slice(2));
|
||||
const profile = buildPicoclawProfile({
|
||||
picoclawHome: args.home,
|
||||
generatedAt: args.generatedAt,
|
||||
configPath: args.config,
|
||||
watchFiles: args.watch,
|
||||
releaseArtifacts: args.artifact,
|
||||
});
|
||||
const out = confineOutputToPicoclawHome(args.output, args.home);
|
||||
fs.mkdirSync(path.dirname(out), { recursive: true, mode: 0o700 });
|
||||
const checkedOut = confineOutputToPicoclawHome(out, args.home);
|
||||
writeNoFollow(checkedOut, `${stableStringify(profile)}\n`);
|
||||
console.log(stableStringify({ message: "picoclaw profile generated", output: checkedOut, canonical_sha256: profile.digests.canonical_sha256 }, 0));
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env node
|
||||
import { verifySupplyChain } from "../lib/supply_chain.mjs";
|
||||
import { stableStringify } from "../lib/profile.mjs";
|
||||
|
||||
function parse(argv) {
|
||||
const args = { allowUnsignedChecksums: false, manifestName: null };
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const token = argv[i];
|
||||
if (token === "--artifact") args.artifactPath = argv[++i];
|
||||
else if (token === "--checksums") args.checksumsPath = argv[++i];
|
||||
else if (token === "--signature") args.signaturePath = argv[++i];
|
||||
else if (token === "--public-key") args.publicKeyPath = argv[++i];
|
||||
else if (token === "--manifest-name") args.manifestName = argv[++i];
|
||||
else if (token === "--allow-unsigned-checksums") args.allowUnsignedChecksums = true;
|
||||
else throw new Error(`Unknown argument: ${token}`);
|
||||
}
|
||||
if (!args.artifactPath || !args.checksumsPath) throw new Error("--artifact and --checksums are required");
|
||||
return args;
|
||||
}
|
||||
const result = verifySupplyChain(parse(process.argv.slice(2)));
|
||||
console.log(stableStringify(result));
|
||||
if (!result.ok) process.exit(2);
|
||||
@@ -0,0 +1,150 @@
|
||||
{
|
||||
"name": "picoclaw-security-guardian",
|
||||
"version": "0.0.1",
|
||||
"description": "Picoclaw security posture skill with advisory awareness, configuration drift detection, and supply-chain verification guidance.",
|
||||
"author": "prompt-security",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"homepage": "https://clawsec.prompt.security/",
|
||||
"platform": "picoclaw",
|
||||
"keywords": [
|
||||
"security",
|
||||
"picoclaw",
|
||||
"ai-gateway",
|
||||
"advisory",
|
||||
"drift-detection",
|
||||
"supply-chain"
|
||||
],
|
||||
"sbom": {
|
||||
"files": [
|
||||
{
|
||||
"path": "SKILL.md",
|
||||
"required": true,
|
||||
"description": "Skill documentation and Picoclaw operator playbook"
|
||||
},
|
||||
{
|
||||
"path": "README.md",
|
||||
"required": true,
|
||||
"description": "Human-oriented overview and quickstart"
|
||||
},
|
||||
{
|
||||
"path": "CHANGELOG.md",
|
||||
"required": true,
|
||||
"description": "Version history and release notes"
|
||||
},
|
||||
{
|
||||
"path": "lib/profile.mjs",
|
||||
"required": true,
|
||||
"description": "Picoclaw posture profile and path-confinement helpers"
|
||||
},
|
||||
{
|
||||
"path": "lib/drift.mjs",
|
||||
"required": true,
|
||||
"description": "Baseline comparison and severity mapping helpers"
|
||||
},
|
||||
{
|
||||
"path": "lib/supply_chain.mjs",
|
||||
"required": true,
|
||||
"description": "Release artifact checksum/signature verification helpers"
|
||||
},
|
||||
{
|
||||
"path": "lib/advisories.mjs",
|
||||
"required": true,
|
||||
"description": "Picoclaw advisory feed filtering helpers"
|
||||
},
|
||||
{
|
||||
"path": "scripts/generate_profile.mjs",
|
||||
"required": true,
|
||||
"description": "Generate deterministic Picoclaw security posture profile"
|
||||
},
|
||||
{
|
||||
"path": "scripts/check_drift.mjs",
|
||||
"required": true,
|
||||
"description": "Compare Picoclaw profile against an approved baseline"
|
||||
},
|
||||
{
|
||||
"path": "scripts/verify_supply_chain.mjs",
|
||||
"required": true,
|
||||
"description": "Verify release artifact checksums and required detached signatures for provenance"
|
||||
},
|
||||
{
|
||||
"path": "scripts/check_advisories.mjs",
|
||||
"required": true,
|
||||
"description": "Check Picoclaw-relevant advisories from a signed/verified feed state"
|
||||
},
|
||||
{
|
||||
"path": "test/profile.test.mjs",
|
||||
"required": false,
|
||||
"description": "Profile generation and path-safety tests"
|
||||
},
|
||||
{
|
||||
"path": "test/drift.test.mjs",
|
||||
"required": false,
|
||||
"description": "Drift severity tests"
|
||||
},
|
||||
{
|
||||
"path": "test/supply_chain.test.mjs",
|
||||
"required": false,
|
||||
"description": "Checksum and required-signature verification tests"
|
||||
},
|
||||
{
|
||||
"path": "test/picoclaw_security_guardian_sandbox_regression.sh",
|
||||
"required": false,
|
||||
"description": "Isolated Docker/Picoclaw install regression harness using Picoclaw find_skills/install_skill and skill-loader validation for pre-release checks"
|
||||
}
|
||||
]
|
||||
},
|
||||
"picoclaw": {
|
||||
"emoji": "\ud83e\udd90",
|
||||
"category": "security",
|
||||
"requires": {
|
||||
"bins": [
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"runtime": {
|
||||
"required_env": [],
|
||||
"optional_env": [
|
||||
"PICOCLAW_HOME",
|
||||
"PICOCLAW_CONFIG",
|
||||
"PICOCLAW_PROFILE_OUTPUT_DIR",
|
||||
"PICOCLAW_BASELINE",
|
||||
"PICOCLAW_ADVISORY_FEED_STATE_PATH",
|
||||
"PICOCLAW_ADVISORY_CACHED_FEED",
|
||||
"PICOCLAW_ALLOW_UNSIGNED_FEED"
|
||||
]
|
||||
},
|
||||
"capabilities": {
|
||||
"security_feed": true,
|
||||
"config_drift": true,
|
||||
"agent_self_pen_testing": false,
|
||||
"supply_chain_install_verification": true
|
||||
},
|
||||
"execution": {
|
||||
"always": false,
|
||||
"persistence": "Read-only/on-demand in v0.0.1; no scheduler is installed.",
|
||||
"network_egress": "None by default. Advisory checks consume local verified feed state/cache unless the operator supplies a feed file."
|
||||
},
|
||||
"operator_review": [
|
||||
"Picoclaw-specific skill: use for Picoclaw gateways and lightweight AI gateway deployments, not OpenClaw hook execution.",
|
||||
"Treat public Web UI binding and broad chat-channel enablement as review findings until explicitly justified.",
|
||||
"Keep unsigned advisory mode temporary and documented; default workflows expect verified feed state.",
|
||||
"Supply-chain verification requires manifests/signatures from a trusted release source; third-party LuCI wrappers need separate provenance review."
|
||||
],
|
||||
"triggers": [
|
||||
"picoclaw security profile",
|
||||
"picoclaw drift detection",
|
||||
"picoclaw advisory check",
|
||||
"picoclaw supply chain verification"
|
||||
],
|
||||
"test_requires": {
|
||||
"bins": [
|
||||
"bash",
|
||||
"docker",
|
||||
"python3",
|
||||
"node",
|
||||
"openssl",
|
||||
"zip"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import assert from "node:assert/strict"; import { diffPicoclawProfiles, highestSeverity } from "../lib/drift.mjs";
|
||||
const base={platform:"picoclaw",schema_version:"picoclaw-profile/v1",posture:{runtime:{ui:{public_web_ui:false,auth_disabled:false},tools:{unrestricted_workspace:false},mcp:{enabled:false},scheduler:{enabled:false},risky_toggles:{allow_unsigned_mode:false},secrets:{config_secret_markers:0}},feed_verification:{status:"verified"},integrity:{watched_files:[{path:"a",sha256:"1"}],release_artifacts:[]}}};
|
||||
const cur=JSON.parse(JSON.stringify(base)); cur.posture.runtime.ui.public_web_ui=true; cur.posture.feed_verification.status="unknown"; cur.posture.integrity.watched_files[0].sha256="2";
|
||||
const d=diffPicoclawProfiles(base,cur); assert.equal(highestSeverity(d.findings),"critical"); assert.ok(d.findings.some(f=>f.code==="PUBLIC_WEB_UI_ENABLED")); assert.ok(d.findings.some(f=>f.code==="FEED_VERIFICATION_REGRESSION")); assert.ok(d.findings.some(f=>f.code==="WATCHED_FILE_DRIFT")); console.log("drift.test.mjs PASS");
|
||||
+371
@@ -0,0 +1,371 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Picoclaw-oriented sandbox regression for picoclaw-security-guardian.
|
||||
#
|
||||
# This is deliberately NOT a Hermes install test. It boots a disposable Docker
|
||||
# sandbox, mounts a Picoclaw source tree, publishes this skill through a local
|
||||
# ClawHub-compatible registry, installs it with Picoclaw's own install_skill tool,
|
||||
# verifies Picoclaw's skill loader can see/load it, then runs the installed copy's
|
||||
# Picoclaw security workflows against an isolated PICOCLAW_HOME.
|
||||
#
|
||||
# Usage from the ClawSec repo root:
|
||||
# skills/picoclaw-security-guardian/test/picoclaw_security_guardian_sandbox_regression.sh
|
||||
#
|
||||
# Optional env overrides:
|
||||
# IMAGE=golang:1.25-bookworm
|
||||
# PICOCLAW_SRC=/home/davida/picoclaw_research/picoclaw
|
||||
# SKILL_SRC=/home/davida/clawsec/skills/picoclaw-security-guardian
|
||||
# CLAWHUB_PORT=8767
|
||||
|
||||
IMAGE="${IMAGE:-golang:1.25-bookworm}"
|
||||
PICOCLAW_SRC="${PICOCLAW_SRC:-$HOME/picoclaw_research/picoclaw}"
|
||||
SKILL_SRC="${SKILL_SRC:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||
CLAWHUB_PORT="${CLAWHUB_PORT:-8767}"
|
||||
SKILL_VERSION="${SKILL_VERSION:-$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1], encoding="utf-8"))["version"])' "$SKILL_SRC/skill.json")}"
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "ERROR: docker is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "$PICOCLAW_SRC" ]]; then
|
||||
echo "ERROR: PICOCLAW_SRC not found: $PICOCLAW_SRC" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$PICOCLAW_SRC/go.mod" ]]; then
|
||||
echo "ERROR: PICOCLAW_SRC does not look like a Picoclaw Go module: $PICOCLAW_SRC" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "$SKILL_SRC" ]]; then
|
||||
echo "ERROR: SKILL_SRC not found: $SKILL_SRC" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[sandbox] image=$IMAGE"
|
||||
echo "[sandbox] picoclaw-src=$PICOCLAW_SRC"
|
||||
echo "[sandbox] skill-src=$SKILL_SRC"
|
||||
echo "[sandbox] skill-version=$SKILL_VERSION"
|
||||
|
||||
docker run --rm \
|
||||
-e HOME=/tmp/picoclaw-user-home \
|
||||
-e PICOCLAW_HOME=/tmp/picoclaw-instance-home \
|
||||
-e PICOCLAW_WORKSPACE=/tmp/picoclaw-workspace \
|
||||
-e SKILL_VERSION="$SKILL_VERSION" \
|
||||
-e CLAWHUB_PORT="$CLAWHUB_PORT" \
|
||||
-v "$PICOCLAW_SRC":/opt/picoclaw-src:ro \
|
||||
-v "$SKILL_SRC":/opt/skill-src:ro \
|
||||
"$IMAGE" bash -lc '
|
||||
set -euo pipefail
|
||||
export PATH="/usr/local/go/bin:$PATH"
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update >/dev/null
|
||||
apt-get install -y --no-install-recommends ca-certificates curl nodejs npm openssl zip >/dev/null
|
||||
|
||||
mkdir -p "$HOME" "$PICOCLAW_HOME/security/clawsec" "$PICOCLAW_WORKSPACE" /tmp/clawhub /tmp/registry-src
|
||||
|
||||
echo "INSIDE_HOME=$HOME"
|
||||
echo "INSIDE_PICOCLAW_HOME=$PICOCLAW_HOME"
|
||||
echo "INSIDE_PICOCLAW_WORKSPACE=$PICOCLAW_WORKSPACE"
|
||||
|
||||
# Build a ClawHub-style archive with SKILL.md at the archive root, because
|
||||
# Picoclaw extracts registry ZIPs directly into workspace/skills/<slug>/.
|
||||
cp /opt/skill-src/SKILL.md /opt/skill-src/README.md /opt/skill-src/CHANGELOG.md /opt/skill-src/skill.json /tmp/registry-src/
|
||||
cp -a /opt/skill-src/lib /opt/skill-src/scripts /tmp/registry-src/
|
||||
(
|
||||
cd /tmp/registry-src
|
||||
zip -qr /tmp/clawhub/picoclaw-security-guardian.zip .
|
||||
)
|
||||
|
||||
ZIP_SHA=$(sha256sum /tmp/clawhub/picoclaw-security-guardian.zip | awk "{print \$1}")
|
||||
cat > /tmp/checksums.json <<EOF
|
||||
{"files":{"picoclaw-security-guardian.zip":{"sha256":"$ZIP_SHA"}}}
|
||||
EOF
|
||||
openssl genpkey -algorithm ed25519 -out /tmp/release-sign.key >/dev/null 2>&1
|
||||
openssl pkey -in /tmp/release-sign.key -pubout -out /tmp/signing-public.pem >/dev/null 2>&1
|
||||
node - <<"NODE"
|
||||
const crypto = require("node:crypto");
|
||||
const fs = require("node:fs");
|
||||
const privateKey = crypto.createPrivateKey(fs.readFileSync("/tmp/release-sign.key"));
|
||||
const manifestBytes = fs.readFileSync("/tmp/checksums.json");
|
||||
fs.writeFileSync("/tmp/checksums.json.sig", crypto.sign(null, manifestBytes, privateKey).toString("base64") + "\n");
|
||||
NODE
|
||||
|
||||
# Release artifact verification preflight: checksum + detached Ed25519 signature.
|
||||
node /opt/skill-src/scripts/verify_supply_chain.mjs \
|
||||
--artifact /tmp/clawhub/picoclaw-security-guardian.zip \
|
||||
--checksums /tmp/checksums.json \
|
||||
--signature /tmp/checksums.json.sig \
|
||||
--public-key /tmp/signing-public.pem >/tmp/release-verify.log
|
||||
|
||||
cat > /tmp/clawhub_server.py <<"PY"
|
||||
import json
|
||||
import os
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
SKILL = "picoclaw-security-guardian"
|
||||
VERSION = os.environ["SKILL_VERSION"]
|
||||
ZIP_PATH = "/tmp/clawhub/picoclaw-security-guardian.zip"
|
||||
SUMMARY = "Picoclaw security posture checks: advisory awareness, config drift, and supply-chain verification."
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, fmt, *args):
|
||||
return
|
||||
|
||||
def send_json(self, obj):
|
||||
body = json.dumps(obj).encode("utf-8")
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self):
|
||||
parsed = urlparse(self.path)
|
||||
if parsed.path == "/api/v1/search":
|
||||
self.send_json({"results": [{"score": 1.0, "slug": SKILL, "displayName": "Picoclaw Security Guardian", "summary": SUMMARY, "version": VERSION}]})
|
||||
return
|
||||
if parsed.path == f"/api/v1/skills/{SKILL}":
|
||||
self.send_json({"slug": SKILL, "displayName": "Picoclaw Security Guardian", "summary": SUMMARY, "latestVersion": {"version": VERSION}, "moderation": {"isMalwareBlocked": False, "isSuspicious": False}})
|
||||
return
|
||||
if parsed.path == "/api/v1/download":
|
||||
qs = parse_qs(parsed.query)
|
||||
if qs.get("slug", [""])[0] != SKILL:
|
||||
self.send_error(404, "unknown skill")
|
||||
return
|
||||
data = open(ZIP_PATH, "rb").read()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/zip")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
return
|
||||
self.send_error(404, "not found")
|
||||
|
||||
ThreadingHTTPServer(("127.0.0.1", int(os.environ["CLAWHUB_PORT"])), Handler).serve_forever()
|
||||
PY
|
||||
python3 /tmp/clawhub_server.py >/tmp/clawhub.log 2>&1 &
|
||||
SERVER_PID=$!
|
||||
trap "kill $SERVER_PID >/dev/null 2>&1 || true; wait $SERVER_PID 2>/dev/null || true" EXIT
|
||||
REGISTRY_READY=0
|
||||
for _ in $(seq 1 30); do
|
||||
if curl -fsS "http://127.0.0.1:$CLAWHUB_PORT/api/v1/skills/picoclaw-security-guardian" >/dev/null; then
|
||||
REGISTRY_READY=1
|
||||
break
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
if [ "$REGISTRY_READY" -ne 1 ]; then
|
||||
echo "ERROR: local ClawHub-compatible registry did not become ready" >&2
|
||||
cat /tmp/clawhub.log >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Exercise Picoclaw itself: registry search -> install_skill -> skill loader.
|
||||
cat > /tmp/picoclaw_skill_harness.go <<"GO"
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/skills"
|
||||
integrationtools "github.com/sipeed/picoclaw/pkg/tools/integration"
|
||||
)
|
||||
|
||||
func must(ok bool, msg string, args ...any) {
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, msg+"\n", args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
workspace := os.Getenv("PICOCLAW_WORKSPACE")
|
||||
baseURL := "http://127.0.0.1:" + os.Getenv("CLAWHUB_PORT")
|
||||
version := os.Getenv("SKILL_VERSION")
|
||||
|
||||
registryMgr := skills.NewRegistryManager()
|
||||
registryMgr.AddRegistry(skills.NewClawHubRegistry(skills.ClawHubConfig{Enabled: true, BaseURL: baseURL, Timeout: 10}))
|
||||
|
||||
findTool := integrationtools.NewFindSkillsTool(registryMgr, skills.NewSearchCache(50, 5*time.Minute))
|
||||
findResult := findTool.Execute(context.Background(), map[string]any{"query": "picoclaw security", "limit": float64(5)})
|
||||
fmt.Println(findResult.ForLLM)
|
||||
must(!findResult.IsError, "find_skills failed: %s", findResult.ForLLM)
|
||||
must(strings.Contains(findResult.ForLLM, "picoclaw-security-guardian"), "find_skills did not return picoclaw-security-guardian")
|
||||
|
||||
installTool := integrationtools.NewInstallSkillTool(registryMgr, workspace)
|
||||
installResult := installTool.Execute(context.Background(), map[string]any{
|
||||
"slug": "picoclaw-security-guardian",
|
||||
"registry": "clawhub",
|
||||
"version": version,
|
||||
})
|
||||
fmt.Println(installResult.ForLLM)
|
||||
must(!installResult.IsError, "install_skill failed: %s", installResult.ForLLM)
|
||||
must(strings.Contains(installResult.ForLLM, "Successfully installed skill"), "install_skill did not report success")
|
||||
|
||||
installed := filepath.Join(workspace, "skills", "picoclaw-security-guardian")
|
||||
for _, rel := range []string{"SKILL.md", "skill.json", "scripts/generate_profile.mjs", "scripts/check_drift.mjs", "scripts/check_advisories.mjs", "scripts/verify_supply_chain.mjs"} {
|
||||
if _, err := os.Stat(filepath.Join(installed, rel)); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "missing installed file %s: %v\n", rel, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
loader := skills.NewSkillsLoader(workspace, filepath.Join(os.Getenv("PICOCLAW_HOME"), "skills"), "")
|
||||
found := false
|
||||
for _, skill := range loader.ListSkills() {
|
||||
if skill.Name == "picoclaw-security-guardian" && skill.Source == "workspace" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
must(found, "Picoclaw SkillsLoader did not list installed picoclaw-security-guardian workspace skill")
|
||||
content, ok := loader.LoadSkill("picoclaw-security-guardian")
|
||||
must(ok, "Picoclaw SkillsLoader could not load installed skill content")
|
||||
must(strings.Contains(content, "Picoclaw Security Guardian"), "loaded skill content is not Picoclaw Security Guardian")
|
||||
|
||||
fmt.Println("picoclaw_find_skill=PASS")
|
||||
fmt.Println("picoclaw_install_skill=PASS")
|
||||
fmt.Println("picoclaw_skill_loader=PASS")
|
||||
}
|
||||
GO
|
||||
(
|
||||
cd /opt/picoclaw-src
|
||||
go run /tmp/picoclaw_skill_harness.go >/tmp/picoclaw-install.log
|
||||
)
|
||||
cat /tmp/picoclaw-install.log
|
||||
|
||||
SKILL_DIR="$PICOCLAW_WORKSPACE/skills/picoclaw-security-guardian"
|
||||
|
||||
# Use Picoclaw-native config paths and shapes: config.json + launcher-config.json.
|
||||
cat > "$PICOCLAW_HOME/config.json" <<EOF
|
||||
{
|
||||
"version": 3,
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"workspace": "$PICOCLAW_WORKSPACE",
|
||||
"restrict_to_workspace": true,
|
||||
"model_name": "sandbox-model"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"exec": {"enabled": false},
|
||||
"cron": {"enabled": false},
|
||||
"find_skills": {"enabled": true},
|
||||
"install_skill": {"enabled": true}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
cat > "$PICOCLAW_HOME/launcher-config.json" <<EOF
|
||||
{
|
||||
"port": 18800,
|
||||
"public": false,
|
||||
"allowed_cidrs": ["127.0.0.1/32"],
|
||||
"dashboard_password_hash": "argon2id-test-hash"
|
||||
}
|
||||
EOF
|
||||
|
||||
node "$SKILL_DIR/scripts/generate_profile.mjs" \
|
||||
--home "$PICOCLAW_HOME" \
|
||||
--output "$PICOCLAW_HOME/security/clawsec/baseline-profile.json" \
|
||||
--generated-at 2026-04-25T00:00:00.000Z >/tmp/profile-baseline.log
|
||||
|
||||
cp "$PICOCLAW_HOME/security/clawsec/baseline-profile.json" "$PICOCLAW_HOME/security/clawsec/current-profile.json"
|
||||
node "$SKILL_DIR/scripts/check_drift.mjs" \
|
||||
--baseline "$PICOCLAW_HOME/security/clawsec/baseline-profile.json" \
|
||||
--current "$PICOCLAW_HOME/security/clawsec/current-profile.json" \
|
||||
--fail-on critical >/tmp/drift-clean.log
|
||||
|
||||
cat > "$PICOCLAW_HOME/config.json" <<EOF
|
||||
{
|
||||
"version": 3,
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"workspace": "/",
|
||||
"restrict_to_workspace": false,
|
||||
"allow_read_outside_workspace": true,
|
||||
"model_name": "sandbox-model"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"exec": {"enabled": true, "allow_remote": true},
|
||||
"cron": {"enabled": true, "allow_command": true},
|
||||
"mcp": {
|
||||
"enabled": true,
|
||||
"servers": {
|
||||
"dangerous-local": {"command": "node", "args": ["server.js"]}
|
||||
}
|
||||
},
|
||||
"web": {"brave": {"enabled": true, "api_keys": ["test-secret-value"]}}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
cat > "$PICOCLAW_HOME/launcher-config.json" <<EOF
|
||||
{
|
||||
"port": 18800,
|
||||
"public": true,
|
||||
"allowed_cidrs": ["0.0.0.0/0"],
|
||||
"dashboard_password_hash": ""
|
||||
}
|
||||
EOF
|
||||
node "$SKILL_DIR/scripts/generate_profile.mjs" \
|
||||
--home "$PICOCLAW_HOME" \
|
||||
--output "$PICOCLAW_HOME/security/clawsec/current-profile.json" \
|
||||
--generated-at 2026-04-25T00:10:00.000Z >/tmp/profile-current.log
|
||||
|
||||
set +e
|
||||
DRIFT_OUT=$(node "$SKILL_DIR/scripts/check_drift.mjs" \
|
||||
--baseline "$PICOCLAW_HOME/security/clawsec/baseline-profile.json" \
|
||||
--current "$PICOCLAW_HOME/security/clawsec/current-profile.json" \
|
||||
--fail-on critical 2>&1)
|
||||
DRIFT_CODE=$?
|
||||
set -e
|
||||
[ "$DRIFT_CODE" -ne 0 ]
|
||||
echo "$DRIFT_OUT" | grep -Eq "PUBLIC_WEB_UI_ENABLED|WEB_UI_AUTH_DISABLED|WORKSPACE_RESTRICTION_DISABLED"
|
||||
|
||||
cat > /tmp/picoclaw-feed.json <<EOF
|
||||
{"version":"1.0.0","updated":"2026-04-25T00:00:00Z","advisories":[{"id":"CLAW-PICO-TEST","severity":"high","type":"prompt_injection","platforms":["picoclaw"],"affected":["picoclaw-security-guardian@$SKILL_VERSION"],"title":"Picoclaw test advisory","description":"Picoclaw gateway review","published":"2026-04-25T00:00:00Z","action":"Review before release"}]}
|
||||
EOF
|
||||
cat > /tmp/feed-state-unknown.json <<EOF
|
||||
{"status":"unknown"}
|
||||
EOF
|
||||
set +e
|
||||
ADVISORY_UNKNOWN_OUT=$(node "$SKILL_DIR/scripts/check_advisories.mjs" --feed /tmp/picoclaw-feed.json --state /tmp/feed-state-unknown.json 2>&1)
|
||||
ADVISORY_UNKNOWN_CODE=$?
|
||||
set -e
|
||||
if [ "$ADVISORY_UNKNOWN_CODE" -eq 0 ]; then
|
||||
echo "ERROR: advisory check unexpectedly allowed unknown feed state" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "$ADVISORY_UNKNOWN_OUT" | grep -q "advisory feed state is not verified"
|
||||
cat > /tmp/feed-state-verified.json <<EOF
|
||||
{"status":"verified"}
|
||||
EOF
|
||||
node "$SKILL_DIR/scripts/check_advisories.mjs" --feed /tmp/picoclaw-feed.json --state /tmp/feed-state-verified.json >/tmp/advisory-verified.log
|
||||
grep -q "CLAW-PICO-TEST" /tmp/advisory-verified.log
|
||||
|
||||
node "$SKILL_DIR/scripts/verify_supply_chain.mjs" \
|
||||
--artifact /tmp/clawhub/picoclaw-security-guardian.zip \
|
||||
--checksums /tmp/checksums.json \
|
||||
--signature /tmp/checksums.json.sig \
|
||||
--public-key /tmp/signing-public.pem >/tmp/installed-supply-chain.log
|
||||
|
||||
echo "=== PICOCLAW SANDBOX FEATURE TEST SUMMARY ==="
|
||||
echo "picoclaw_find_skill=PASS"
|
||||
echo "picoclaw_install_skill=PASS"
|
||||
echo "picoclaw_skill_loader=PASS"
|
||||
echo "release_verify_triad=PASS"
|
||||
echo "generate_profile=PASS"
|
||||
echo "picoclaw_json_config_detection=PASS"
|
||||
echo "clean_drift_pass=PASS"
|
||||
echo "baseline_drift_fail_closed=PASS"
|
||||
echo "advisory_unknown_state_fail_closed=PASS"
|
||||
echo "advisory_verified_filter=PASS"
|
||||
echo "installed_supply_chain_verify=PASS"
|
||||
echo "[sandbox] completed successfully"
|
||||
'
|
||||
@@ -0,0 +1,16 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { buildPicoclawProfile, confineOutputToPicoclawHome } from "../lib/profile.mjs";
|
||||
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "picoclaw-profile-"));
|
||||
fs.writeFileSync(path.join(dir, "config.yaml"), "bind: 0.0.0.0\nauth: false\nmcp:\n", "utf8");
|
||||
const profile = buildPicoclawProfile({ picoclawHome: dir, generatedAt: "2026-04-25T00:00:00.000Z" });
|
||||
assert.equal(profile.platform, "picoclaw");
|
||||
assert.equal(profile.posture.runtime.ui.public_web_ui, true);
|
||||
assert.equal(profile.posture.runtime.ui.auth_disabled, true);
|
||||
assert.equal(profile.posture.runtime.mcp.enabled, true);
|
||||
assert.match(profile.digests.canonical_sha256, /^[a-f0-9]{64}$/);
|
||||
assert.throws(() => confineOutputToPicoclawHome(path.join(dir, "..", "escape.json"), dir), /must stay under/);
|
||||
console.log("profile.test.mjs PASS");
|
||||
@@ -0,0 +1,25 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { verifyChecksums, verifyDetachedSignature, verifySupplyChain } from "../lib/supply_chain.mjs";
|
||||
import { sha256FileHex } from "../lib/profile.mjs";
|
||||
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "picoclaw-supply-"));
|
||||
const artifact = path.join(dir, "picoclaw");
|
||||
fs.writeFileSync(artifact, "binary", "utf8");
|
||||
const manifest = path.join(dir, "checksums.json");
|
||||
fs.writeFileSync(manifest, JSON.stringify({ files: { picoclaw: { sha256: sha256FileHex(artifact) } } }), "utf8");
|
||||
assert.equal(verifyChecksums({ artifactPath: artifact, checksumsPath: manifest }).ok, true);
|
||||
assert.equal(verifySupplyChain({ artifactPath: artifact, checksumsPath: manifest }).ok, false);
|
||||
|
||||
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
||||
const sig = crypto.sign(null, fs.readFileSync(manifest), privateKey).toString("base64");
|
||||
const pub = path.join(dir, "pub.pem");
|
||||
const sigPath = path.join(dir, "checksums.json.sig");
|
||||
fs.writeFileSync(pub, publicKey.export({ type: "spki", format: "pem" }));
|
||||
fs.writeFileSync(sigPath, sig);
|
||||
assert.equal(verifyDetachedSignature({ manifestPath: manifest, signaturePath: sigPath, publicKeyPath: pub }).ok, true);
|
||||
assert.equal(verifySupplyChain({ artifactPath: artifact, checksumsPath: manifest, signaturePath: sigPath, publicKeyPath: pub }).ok, true);
|
||||
console.log("supply_chain.test.mjs PASS");
|
||||
@@ -0,0 +1,8 @@
|
||||
# Changelog
|
||||
|
||||
## [0.0.1] - 2026-04-26
|
||||
|
||||
### Added
|
||||
- Initial extraction from `picoclaw-security-guardian` to isolate self-pen-testing checks as a standalone Picoclaw skill.
|
||||
- Local read-only finding engine (`lib/self_pen_test.mjs`).
|
||||
- CLI runner (`scripts/self_pen_test.mjs`) and unit test (`test/self_pen_test.test.mjs`).
|
||||
@@ -0,0 +1,21 @@
|
||||
# picoclaw-self-pen-testing
|
||||
|
||||
Picoclaw-only local posture-review findings package for ClawSec.
|
||||
|
||||
Status: implemented (v0.0.1), Picoclaw-specific.
|
||||
|
||||
## What it does
|
||||
|
||||
Given a generated Picoclaw posture profile, it emits severity-ranked findings and a summary count for local operator review.
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
node scripts/self_pen_test.mjs --profile ~/.picoclaw/security/clawsec/current-profile.json
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
node test/self_pen_test.test.mjs
|
||||
```
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: picoclaw-self-pen-testing
|
||||
version: 0.0.1
|
||||
description: Picoclaw-only local posture-review skill focused on read-only findings and safe operator remediation guidance.
|
||||
homepage: https://clawsec.prompt.security
|
||||
author: prompt-security
|
||||
license: AGPL-3.0-or-later
|
||||
picoclaw:
|
||||
emoji: "🦐"
|
||||
category: "security"
|
||||
requires:
|
||||
bins: [node]
|
||||
test_requires:
|
||||
bins: [node]
|
||||
---
|
||||
|
||||
# Picoclaw Posture Review (separate package)
|
||||
|
||||
Purpose: keep Picoclaw posture-review checks isolated from the broader guardian package so moderation-sensitive checks can be versioned/published independently.
|
||||
|
||||
## Scope
|
||||
|
||||
This skill only performs local, read-only posture-review analysis against an existing Picoclaw posture profile.
|
||||
|
||||
It flags:
|
||||
- public Web UI exposure
|
||||
- disabled UI auth
|
||||
- unrestricted workspace/tooling
|
||||
- unsigned verification mode
|
||||
- MCP trust-boundary review needs
|
||||
- scheduler persistence review
|
||||
- plaintext secret markers
|
||||
- multi-channel auth review
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
node scripts/self_pen_test.mjs --profile ~/.picoclaw/security/clawsec/current-profile.json
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
```bash
|
||||
python utils/validate_skill.py skills/picoclaw-self-pen-testing
|
||||
node skills/picoclaw-self-pen-testing/test/self_pen_test.test.mjs
|
||||
```
|
||||
@@ -0,0 +1,11 @@
|
||||
export function stableStringify(value, space = 2) {
|
||||
return JSON.stringify(sortDeep(value), null, space);
|
||||
}
|
||||
|
||||
function sortDeep(value) {
|
||||
if (Array.isArray(value)) return value.map(sortDeep);
|
||||
if (!value || typeof value !== "object") return value;
|
||||
const out = {};
|
||||
for (const key of Object.keys(value).sort()) out[key] = sortDeep(value[key]);
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
function add(findings, severity, code, title, evidence, recommendation) { findings.push({ severity, code, title, evidence, recommendation }); }
|
||||
export function runPicoclawSelfPenTest(profile, _options = {}) {
|
||||
const findings=[]; const rt=profile?.posture?.runtime || {};
|
||||
if (rt.ui?.public_web_ui) add(findings,"critical","PUBLIC_WEB_UI_EXPOSED","Web UI appears bound publicly","public_web_ui=true or equivalent detected","Bind to localhost or enforce password auth + CIDR allowlist before exposure.");
|
||||
if (rt.ui?.auth_disabled) add(findings,"critical","WEB_UI_AUTH_DISABLED","Web UI auth appears disabled","auth_disabled=true or empty password marker detected","Require password/session auth for any gateway controller UI.");
|
||||
if (rt.tools?.unrestricted_workspace) add(findings,"critical","WORKSPACE_UNRESTRICTED","Tool workspace restriction appears disabled","restrict_to_workspace=false or sandbox=false marker detected","Enable workspace confinement and deny symlink/absolute-path escapes.");
|
||||
if (rt.risky_toggles?.allow_unsigned_mode) add(findings,"critical","UNSIGNED_MODE_ALLOWED","Unsigned or insecure verification mode appears enabled","allow_unsigned/skip_signature marker detected","Disable unsigned mode except short audited break-glass windows.");
|
||||
if (rt.mcp?.enabled) add(findings,"high","MCP_REVIEW_REQUIRED","MCP servers enabled","mcp marker detected","Review each MCP server as a separate trust boundary with least privilege and secrets isolation.");
|
||||
if (rt.tools?.enabled) add(findings,"medium","TOOLING_REVIEW_REQUIRED","Agent tools appear enabled","tools/code_execution/shell/filesystem marker detected","Require per-tool allowlists and operator approval for dangerous tools.");
|
||||
if (rt.scheduler?.enabled) add(findings,"medium","SCHEDULER_REVIEW_REQUIRED","Scheduler/persistence features appear enabled","cron/schedule marker detected","Inventory jobs and alert on new persistent actions.");
|
||||
if ((rt.secrets?.config_secret_markers || 0) > 0) add(findings,"high","PLAINTEXT_SECRET_MARKERS","Config contains secret-like markers",`${rt.secrets.config_secret_markers} marker(s) found`,`Move secrets to supported encrypted/secure storage and redact logs/exports.`);
|
||||
const enabledGateways = Object.entries(rt.gateways || {}).filter(([,v])=>!!v).map(([k])=>k);
|
||||
if (enabledGateways.length > 1) add(findings,"medium","MULTI_CHANNEL_AUTH_REVIEW","Multiple chat gateways appear enabled",enabledGateways.join(", "),"Pin immutable user IDs per channel and reject group/forwarded-message ambiguity.");
|
||||
return { summary: summarize(findings), findings };
|
||||
}
|
||||
function summarize(findings) { const out={critical:0, high:0, medium:0, low:0, info:0}; for (const f of findings) out[f.severity]=(out[f.severity]||0)+1; return out; }
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import { runPicoclawSelfPenTest } from "../lib/self_pen_test.mjs";
|
||||
import { stableStringify } from "../lib/format.mjs";
|
||||
|
||||
const idx = process.argv.indexOf("--profile");
|
||||
if (idx < 0 || !process.argv[idx + 1]) throw new Error("--profile is required");
|
||||
|
||||
const profile = JSON.parse(fs.readFileSync(process.argv[idx + 1], "utf8"));
|
||||
const result = runPicoclawSelfPenTest(profile);
|
||||
console.log(stableStringify(result));
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"name": "picoclaw-self-pen-testing",
|
||||
"version": "0.0.1",
|
||||
"description": "Picoclaw-only local posture-review skill focused on read-only findings and safe operator remediation guidance.",
|
||||
"author": "prompt-security",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"homepage": "https://clawsec.prompt.security/",
|
||||
"platform": "picoclaw",
|
||||
"keywords": [
|
||||
"security",
|
||||
"picoclaw",
|
||||
"posture-review",
|
||||
"read-only-audit",
|
||||
"mcp",
|
||||
"auth"
|
||||
],
|
||||
"sbom": {
|
||||
"files": [
|
||||
{
|
||||
"path": "SKILL.md",
|
||||
"required": true,
|
||||
"description": "Skill documentation and operator guidance"
|
||||
},
|
||||
{
|
||||
"path": "README.md",
|
||||
"required": true,
|
||||
"description": "Quickstart overview"
|
||||
},
|
||||
{
|
||||
"path": "CHANGELOG.md",
|
||||
"required": true,
|
||||
"description": "Version history"
|
||||
},
|
||||
{
|
||||
"path": "lib/self_pen_test.mjs",
|
||||
"required": true,
|
||||
"description": "Local posture-review finding engine"
|
||||
},
|
||||
{
|
||||
"path": "lib/format.mjs",
|
||||
"required": true,
|
||||
"description": "Stable JSON formatter for deterministic output"
|
||||
},
|
||||
{
|
||||
"path": "scripts/self_pen_test.mjs",
|
||||
"required": true,
|
||||
"description": "Run posture-review checks on a profile"
|
||||
},
|
||||
{
|
||||
"path": "test/self_pen_test.test.mjs",
|
||||
"required": false,
|
||||
"description": "Finding classification tests"
|
||||
}
|
||||
]
|
||||
},
|
||||
"picoclaw": {
|
||||
"emoji": "🦐",
|
||||
"category": "security",
|
||||
"requires": {
|
||||
"bins": [
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"runtime": {
|
||||
"required_env": [],
|
||||
"optional_env": [
|
||||
"PICOCLAW_HOME"
|
||||
]
|
||||
},
|
||||
"capabilities": {
|
||||
"security_feed": false,
|
||||
"config_drift": false,
|
||||
"agent_self_pen_testing": true,
|
||||
"supply_chain_install_verification": false
|
||||
},
|
||||
"execution": {
|
||||
"always": false,
|
||||
"persistence": "Read-only/on-demand; no scheduler is installed.",
|
||||
"network_egress": "None"
|
||||
},
|
||||
"operator_review": [
|
||||
"This package is intentionally isolated so posture-review checks can be independently published or withheld.",
|
||||
"Treat findings as operator review guidance; do not auto-remediate without explicit approval."
|
||||
],
|
||||
"triggers": [
|
||||
"picoclaw posture review",
|
||||
"picoclaw local security review",
|
||||
"picoclaw auth exposure review"
|
||||
],
|
||||
"test_requires": {
|
||||
"bins": [
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import assert from "node:assert/strict"; import { runPicoclawSelfPenTest } from "../lib/self_pen_test.mjs";
|
||||
const result=runPicoclawSelfPenTest({posture:{runtime:{ui:{public_web_ui:true,auth_disabled:true},tools:{enabled:true,unrestricted_workspace:true},mcp:{enabled:true},scheduler:{enabled:true},risky_toggles:{allow_unsigned_mode:true},secrets:{config_secret_markers:2},gateways:{telegram:true,discord:true}}}});
|
||||
assert.ok(result.summary.critical>=4); assert.ok(result.findings.some(f=>f.code==="MCP_REVIEW_REQUIRED")); assert.ok(result.findings.some(f=>f.code==="MULTI_CHANNEL_AUTH_REVIEW")); console.log("self_pen_test.test.mjs PASS");
|
||||
@@ -28,7 +28,7 @@ export type AdvisoryType =
|
||||
// Keep this open for new categories without requiring type updates.
|
||||
| string;
|
||||
|
||||
export const CORE_PLATFORM_SLUGS = ['openclaw', 'nanoclaw', 'hermes'] as const;
|
||||
export const CORE_PLATFORM_SLUGS = ['openclaw', 'nanoclaw', 'hermes', 'picoclaw'] as const;
|
||||
export type CorePlatformSlug = (typeof CORE_PLATFORM_SLUGS)[number];
|
||||
export type AdvisoryPlatformSlug = CorePlatformSlug | (string & {});
|
||||
export type AdvisoryPlatformFilter = 'all' | CorePlatformSlug | 'other';
|
||||
|
||||
@@ -20,6 +20,10 @@ const PLATFORM_DESCRIPTOR_BY_SLUG: Record<string, PlatformDescriptor> = {
|
||||
label: 'Hermes',
|
||||
classes: 'bg-emerald-500/20 text-emerald-300 border border-emerald-400/40',
|
||||
},
|
||||
picoclaw: {
|
||||
label: 'Picoclaw',
|
||||
classes: 'bg-cyan-500/20 text-cyan-300 border border-cyan-400/40',
|
||||
},
|
||||
};
|
||||
|
||||
const CORE_PLATFORM_SET = new Set<string>(CORE_PLATFORM_SLUGS);
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
- Added a dedicated module page for `clawsec-scanner` and linked it from `wiki/INDEX.md`.
|
||||
- Future updates should preserve existing headings and append `Update Notes` sections when making deltas.
|
||||
- 2026-04-15: Expanded `wiki/modules/hermes-attestation-guardian.md` into full narrative claim breakdowns (people-speak + wiring + verification + scenario) and moved draft-plan context into `wiki/modules/hermes-attestation-guardian-draft-history.md`.
|
||||
- 2026-04-26: Split Picoclaw self-pen-testing into dedicated `wiki/modules/picoclaw-self-pen-testing.md`, and updated `wiki/modules/picoclaw-security-guardian.md` to cover advisory/drift/supply-chain scope only.
|
||||
- 2026-04-25: Added DeepWiki-friendly `wiki/modules/picoclaw-security-guardian.md` with support-matrix claims, threat model, default safety posture, frontend/advisory-board wiring, verification commands, and source references. Regenerated `public/wiki/**/llms.txt` exports with `npm run gen:wiki-llms`.
|
||||
|
||||
## Source References
|
||||
- README.md
|
||||
@@ -24,6 +26,8 @@
|
||||
- wiki/overview.md
|
||||
- wiki/architecture.md
|
||||
- wiki/modules/clawsec-scanner.md
|
||||
- wiki/modules/picoclaw-security-guardian.md
|
||||
- wiki/modules/picoclaw-self-pen-testing.md
|
||||
- wiki/dependencies.md
|
||||
- wiki/data-flow.md
|
||||
- wiki/glossary.md
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
- [Hermes Attestation Guardian](modules/hermes-attestation-guardian.md)
|
||||
- [Hermes Attestation Guardian Draft History (Archived)](modules/hermes-attestation-guardian-draft-history.md)
|
||||
- [NanoClaw Integration](modules/nanoclaw-integration.md)
|
||||
- [Picoclaw Security Guardian](modules/picoclaw-security-guardian.md)
|
||||
- [Picoclaw Self Pen Testing](modules/picoclaw-self-pen-testing.md)
|
||||
- [Automation and Release Pipelines](modules/automation-release.md)
|
||||
- [Local Validation and Packaging Tools](modules/local-tooling.md)
|
||||
|
||||
@@ -42,6 +44,8 @@
|
||||
- [Generation Metadata](GENERATION.md)
|
||||
|
||||
## Update Notes
|
||||
- 2026-04-26: Split Picoclaw self-pen-testing into standalone `picoclaw-self-pen-testing`; updated Picoclaw module docs and references.
|
||||
- 2026-04-25: Added Picoclaw Security Guardian module for advisory awareness, config drift detection, and chain-of-supply verification.
|
||||
- 2026-04-19: Moved NanoClaw platform-support and CI/CD pipeline detail sections out of `README.md` into module pages (`modules/nanoclaw-integration.md`, `modules/automation-release.md`) and left README pointers.
|
||||
- 2026-04-16: Added install-guard compatibility note for Hermes Attestation Guardian (community-source install now SAFE without `--force`; behavior unchanged).
|
||||
- 2026-04-15: Expanded Hermes Attestation Guardian module page into full narrative, claim-by-claim operator guidance (no claim tables), and added archived draft-history module page.
|
||||
@@ -58,7 +62,11 @@
|
||||
- skills/clawsec-suite/skill.json
|
||||
- skills/clawsec-scanner/skill.json
|
||||
- skills/hermes-attestation-guardian/skill.json
|
||||
- skills/picoclaw-security-guardian/skill.json
|
||||
- skills/picoclaw-self-pen-testing/skill.json
|
||||
- wiki/modules/clawsec-scanner.md
|
||||
- wiki/modules/hermes-attestation-guardian.md
|
||||
- wiki/modules/hermes-attestation-guardian-draft-history.md
|
||||
- wiki/modules/picoclaw-security-guardian.md
|
||||
- wiki/modules/picoclaw-self-pen-testing.md
|
||||
- .github/workflows/ci.yml
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# Picoclaw Security Guardian
|
||||
|
||||
## Summary
|
||||
|
||||
Current package version: `v0.0.1`.
|
||||
|
||||
`picoclaw-security-guardian` is the core Picoclaw package for:
|
||||
1. advisory awareness (fail-closed on unverified feed state),
|
||||
2. deterministic profile generation + drift detection,
|
||||
3. release artifact supply-chain verification.
|
||||
|
||||
Self-pen-testing checks were intentionally split out into `picoclaw-self-pen-testing` so moderation-sensitive logic can be published/managed independently.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
- Filter Picoclaw-relevant advisories from verified ClawSec feed state/cache.
|
||||
- Build deterministic posture profiles from Picoclaw config/security files and optional release artifacts.
|
||||
- Compare baseline vs current profile with severity-ranked findings.
|
||||
- Verify release artifacts with checksum manifest + required detached signature for passing provenance verdicts.
|
||||
|
||||
## Default safety posture
|
||||
|
||||
- Read-only by default
|
||||
- No scheduler creation
|
||||
- No outbound network by default
|
||||
- Advisory checks fail closed unless verification state is `verified` (or explicit `--allow-unsigned` override)
|
||||
- Supply-chain verification requires detached-signature verification for a passing provenance result
|
||||
|
||||
## Verification commands
|
||||
|
||||
```bash
|
||||
python utils/validate_skill.py skills/picoclaw-security-guardian
|
||||
node skills/picoclaw-security-guardian/test/profile.test.mjs
|
||||
node skills/picoclaw-security-guardian/test/drift.test.mjs
|
||||
node skills/picoclaw-security-guardian/test/supply_chain.test.mjs
|
||||
bash -n skills/picoclaw-security-guardian/test/picoclaw_security_guardian_sandbox_regression.sh
|
||||
```
|
||||
|
||||
## Picoclaw-native sandbox regression
|
||||
|
||||
`skills/picoclaw-security-guardian/test/picoclaw_security_guardian_sandbox_regression.sh` publishes the package via a local ClawHub-compatible registry, installs through Picoclaw `find_skills` / `install_skill`, validates skill-loader visibility, and runs installed profile/drift/advisory/supply-chain flows against isolated Picoclaw fixtures.
|
||||
|
||||
## Related package
|
||||
|
||||
- `skills/picoclaw-self-pen-testing/` (optional separate self-pen-testing package)
|
||||
|
||||
## Source references
|
||||
|
||||
- `skills/picoclaw-security-guardian/skill.json`
|
||||
- `skills/picoclaw-security-guardian/SKILL.md`
|
||||
- `skills/picoclaw-security-guardian/README.md`
|
||||
- `skills/picoclaw-security-guardian/lib/profile.mjs`
|
||||
- `skills/picoclaw-security-guardian/lib/drift.mjs`
|
||||
- `skills/picoclaw-security-guardian/lib/advisories.mjs`
|
||||
- `skills/picoclaw-security-guardian/lib/supply_chain.mjs`
|
||||
- `skills/picoclaw-security-guardian/scripts/generate_profile.mjs`
|
||||
- `skills/picoclaw-security-guardian/scripts/check_drift.mjs`
|
||||
- `skills/picoclaw-security-guardian/scripts/check_advisories.mjs`
|
||||
- `skills/picoclaw-security-guardian/scripts/verify_supply_chain.mjs`
|
||||
- `skills/picoclaw-security-guardian/test/profile.test.mjs`
|
||||
- `skills/picoclaw-security-guardian/test/drift.test.mjs`
|
||||
- `skills/picoclaw-security-guardian/test/supply_chain.test.mjs`
|
||||
- `skills/picoclaw-security-guardian/test/picoclaw_security_guardian_sandbox_regression.sh`
|
||||
@@ -0,0 +1,44 @@
|
||||
# Picoclaw Self Pen Testing
|
||||
|
||||
## Summary
|
||||
|
||||
Current package version: `v0.0.1`.
|
||||
|
||||
`picoclaw-self-pen-testing` is a standalone Picoclaw package that runs local, read-only self-pen-testing style checks from a generated Picoclaw posture profile.
|
||||
|
||||
This package is intentionally separate from `picoclaw-security-guardian` so moderation-sensitive findings can be shipped independently.
|
||||
|
||||
## What it checks
|
||||
|
||||
- Public Web UI exposure
|
||||
- Disabled Web UI auth
|
||||
- Unrestricted workspace/tooling posture
|
||||
- Unsafely unsigned verification mode
|
||||
- MCP trust-boundary review needs
|
||||
- Scheduler persistence review
|
||||
- Plaintext secret markers
|
||||
- Multi-channel auth review
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
node skills/picoclaw-self-pen-testing/scripts/self_pen_test.mjs \
|
||||
--profile ~/.picoclaw/security/clawsec/current-profile.json
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
```bash
|
||||
python utils/validate_skill.py skills/picoclaw-self-pen-testing
|
||||
node skills/picoclaw-self-pen-testing/test/self_pen_test.test.mjs
|
||||
```
|
||||
|
||||
## Source references
|
||||
|
||||
- `skills/picoclaw-self-pen-testing/skill.json`
|
||||
- `skills/picoclaw-self-pen-testing/SKILL.md`
|
||||
- `skills/picoclaw-self-pen-testing/README.md`
|
||||
- `skills/picoclaw-self-pen-testing/lib/self_pen_test.mjs`
|
||||
- `skills/picoclaw-self-pen-testing/lib/format.mjs`
|
||||
- `skills/picoclaw-self-pen-testing/scripts/self_pen_test.mjs`
|
||||
- `skills/picoclaw-self-pen-testing/test/self_pen_test.test.mjs`
|
||||
Reference in New Issue
Block a user