feat: add collectors and rules
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,3 +5,7 @@ node_modules/
|
|||||||
target/
|
target/
|
||||||
venv/
|
venv/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
30-evidence/
|
||||||
|
50-reports/
|
||||||
|
60-proofs/
|
||||||
|
70-violations/
|
||||||
|
|||||||
41
20-collectors/collect_backup_restore_drill.sh
Executable file
41
20-collectors/collect_backup_restore_drill.sh
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
OUT_DIR="${1:?usage: collect_backup_restore_drill.sh <out_dir>}"
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
|
||||||
|
ROOT="../vm-skills"
|
||||||
|
LATEST="$(find "$ROOT" -type f -name "*restore*drill*.json" 2>/dev/null | sort | tail -n 1 || true)"
|
||||||
|
|
||||||
|
file_mtime_epoch() {
|
||||||
|
local file="$1"
|
||||||
|
if stat -c %Y "$file" >/dev/null 2>&1; then
|
||||||
|
stat -c %Y "$file"
|
||||||
|
else
|
||||||
|
stat -f %m "$file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
file_mtime_iso() {
|
||||||
|
local file="$1"
|
||||||
|
local mtime
|
||||||
|
mtime="$(file_mtime_epoch "$file")"
|
||||||
|
if date -u -r "$file" "+%Y-%m-%dT%H:%M:%SZ" >/dev/null 2>&1; then
|
||||||
|
date -u -r "$file" "+%Y-%m-%dT%H:%M:%SZ"
|
||||||
|
elif date -u -d "@${mtime}" "+%Y-%m-%dT%H:%M:%SZ" >/dev/null 2>&1; then
|
||||||
|
date -u -d "@${mtime}" "+%Y-%m-%dT%H:%M:%SZ"
|
||||||
|
else
|
||||||
|
date -u "+%Y-%m-%dT%H:%M:%SZ"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -n "$LATEST" && -f "$LATEST" ]]; then
|
||||||
|
TS="$(file_mtime_iso "$LATEST")"
|
||||||
|
cat > "$OUT_DIR/backup_restore_drill.json" <<JSON
|
||||||
|
{"collected": true, "path": "$LATEST", "observed_at": "$TS"}
|
||||||
|
JSON
|
||||||
|
else
|
||||||
|
cat > "$OUT_DIR/backup_restore_drill.json" <<'JSON'
|
||||||
|
{"collected": false, "reason": "no restore drill artifacts found"}
|
||||||
|
JSON
|
||||||
|
fi
|
||||||
27
20-collectors/collect_constitution_hash.sh
Executable file
27
20-collectors/collect_constitution_hash.sh
Executable file
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
OUT_DIR="${1:?usage: collect_constitution_hash.sh <out_dir>}"
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
|
||||||
|
LOCK_PATH="../vm-mcp/governance/constitution.lock"
|
||||||
|
|
||||||
|
hash_file() {
|
||||||
|
local file="$1"
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then
|
||||||
|
sha256sum "$file" | awk '{print $1}'
|
||||||
|
else
|
||||||
|
shasum -a 256 "$file" | awk '{print $1}'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -f "$LOCK_PATH" ]]; then
|
||||||
|
HASH="$(hash_file "$LOCK_PATH")"
|
||||||
|
cat > "$OUT_DIR/constitution_hash.json" <<JSON
|
||||||
|
{"collected": true, "path": "$LOCK_PATH", "sha256": "$HASH"}
|
||||||
|
JSON
|
||||||
|
else
|
||||||
|
cat > "$OUT_DIR/constitution_hash.json" <<'JSON'
|
||||||
|
{"collected": false, "reason": "constitution.lock not found at expected path"}
|
||||||
|
JSON
|
||||||
|
fi
|
||||||
15
20-collectors/collect_ledger_verify.sh
Executable file
15
20-collectors/collect_ledger_verify.sh
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
OUT_DIR="${1:?usage: collect_ledger_verify.sh <out_dir>}"
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
|
||||||
|
if command -v ledger >/dev/null 2>&1; then
|
||||||
|
ledger verify --format json > "$OUT_DIR/ledger_verify.json"
|
||||||
|
elif command -v ledger-cli >/dev/null 2>&1; then
|
||||||
|
ledger-cli verify --format json > "$OUT_DIR/ledger_verify.json"
|
||||||
|
else
|
||||||
|
cat > "$OUT_DIR/ledger_verify.json" <<'JSON'
|
||||||
|
{"collected": false, "reason": "ledger CLI not found"}
|
||||||
|
JSON
|
||||||
|
fi
|
||||||
91
40-rules/backup_restore_drill_recent.sh
Executable file
91
40-rules/backup_restore_drill_recent.sh
Executable file
@@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../scripts/lib/common.sh"
|
||||||
|
require_cmd jq
|
||||||
|
|
||||||
|
EVID_DIR="${1:?usage: backup_restore_drill_recent.sh <evidence_dir>}"
|
||||||
|
TS="$(iso_utc_now)"
|
||||||
|
FILE="$EVID_DIR/backup_restore_drill.json"
|
||||||
|
|
||||||
|
MAX_DAYS="${VMCC_MAX_RESTORE_DRILL_AGE_DAYS:-7}"
|
||||||
|
|
||||||
|
file_mtime_epoch() {
|
||||||
|
local file="$1"
|
||||||
|
if stat -c %Y "$file" >/dev/null 2>&1; then
|
||||||
|
stat -c %Y "$file"
|
||||||
|
else
|
||||||
|
stat -f %m "$file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ ! -f "$FILE" ]]; then
|
||||||
|
json_emit "$(jq -n --arg ts "$TS" '{
|
||||||
|
version:"1.0.0",
|
||||||
|
rule_id:"backup.restore_drill_recent",
|
||||||
|
control_ids:["BC-01"],
|
||||||
|
passed:false,
|
||||||
|
severity:"MEDIUM",
|
||||||
|
timestamp:$ts,
|
||||||
|
evidence:[{path:"backup_restore_drill.json"}],
|
||||||
|
details:{error:"missing evidence file"}
|
||||||
|
}')"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
COLLECTED="$(jq -r '.collected // false' "$FILE")"
|
||||||
|
if [[ "$COLLECTED" != "true" ]]; then
|
||||||
|
json_emit "$(jq -n --arg ts "$TS" '{
|
||||||
|
version:"1.0.0",
|
||||||
|
rule_id:"backup.restore_drill_recent",
|
||||||
|
control_ids:["BC-01"],
|
||||||
|
passed:false,
|
||||||
|
severity:"MEDIUM",
|
||||||
|
timestamp:$ts,
|
||||||
|
evidence:[{path:"backup_restore_drill.json"}],
|
||||||
|
details:{error:"no restore drill evidence found"}
|
||||||
|
}')"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
PATH_FOUND="$(jq -r '.path // empty' "$FILE")"
|
||||||
|
if [[ -z "$PATH_FOUND" || ! -f "$PATH_FOUND" ]]; then
|
||||||
|
json_emit "$(jq -n --arg ts "$TS" --arg p "$PATH_FOUND" '{
|
||||||
|
version:"1.0.0",
|
||||||
|
rule_id:"backup.restore_drill_recent",
|
||||||
|
control_ids:["BC-01"],
|
||||||
|
passed:false,
|
||||||
|
severity:"MEDIUM",
|
||||||
|
timestamp:$ts,
|
||||||
|
evidence:[{path:"backup_restore_drill.json"}],
|
||||||
|
details:{error:"referenced drill file missing", referenced:$p}
|
||||||
|
}')"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
NOW_EPOCH="$(date -u +%s)"
|
||||||
|
MTIME_EPOCH="$(file_mtime_epoch "$PATH_FOUND")"
|
||||||
|
AGE_DAYS="$(( (NOW_EPOCH - MTIME_EPOCH) / 86400 ))"
|
||||||
|
|
||||||
|
if [[ "$AGE_DAYS" -le "$MAX_DAYS" ]]; then
|
||||||
|
json_emit "$(jq -n --arg ts "$TS" --argjson age "$AGE_DAYS" '{
|
||||||
|
version:"1.0.0",
|
||||||
|
rule_id:"backup.restore_drill_recent",
|
||||||
|
control_ids:["BC-01"],
|
||||||
|
passed:true,
|
||||||
|
severity:"MEDIUM",
|
||||||
|
timestamp:$ts,
|
||||||
|
evidence:[{path:"backup_restore_drill.json"}],
|
||||||
|
details:{age_days:$age}
|
||||||
|
}')"
|
||||||
|
else
|
||||||
|
json_emit "$(jq -n --arg ts "$TS" --argjson age "$AGE_DAYS" --argjson max "$MAX_DAYS" '{
|
||||||
|
version:"1.0.0",
|
||||||
|
rule_id:"backup.restore_drill_recent",
|
||||||
|
control_ids:["BC-01"],
|
||||||
|
passed:false,
|
||||||
|
severity:"MEDIUM",
|
||||||
|
timestamp:$ts,
|
||||||
|
evidence:[{path:"backup_restore_drill.json"}],
|
||||||
|
details:{error:"restore drill too old", age_days:$age, max_days:$max}
|
||||||
|
}')"
|
||||||
|
fi
|
||||||
78
40-rules/governance_constitution_pinned.sh
Executable file
78
40-rules/governance_constitution_pinned.sh
Executable file
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../scripts/lib/common.sh"
|
||||||
|
require_cmd jq
|
||||||
|
|
||||||
|
EVID_DIR="${1:?usage: governance_constitution_pinned.sh <evidence_dir>}"
|
||||||
|
TS="$(iso_utc_now)"
|
||||||
|
FILE="$EVID_DIR/constitution_hash.json"
|
||||||
|
|
||||||
|
PINNED_SHA256="${VMCC_PINNED_CONSTITUTION_SHA256:-}"
|
||||||
|
|
||||||
|
if [[ ! -f "$FILE" ]]; then
|
||||||
|
json_emit "$(jq -n --arg ts "$TS" '{
|
||||||
|
version:"1.0.0",
|
||||||
|
rule_id:"governance.constitution_pinned",
|
||||||
|
control_ids:["GV-01"],
|
||||||
|
passed:false,
|
||||||
|
severity:"HIGH",
|
||||||
|
timestamp:$ts,
|
||||||
|
evidence:[{path:"constitution_hash.json"}],
|
||||||
|
details:{error:"missing evidence file"}
|
||||||
|
}')"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
COLLECTED="$(jq -r '.collected // false' "$FILE")"
|
||||||
|
if [[ "$COLLECTED" != "true" ]]; then
|
||||||
|
json_emit "$(jq -n --arg ts "$TS" '{
|
||||||
|
version:"1.0.0",
|
||||||
|
rule_id:"governance.constitution_pinned",
|
||||||
|
control_ids:["GV-01"],
|
||||||
|
passed:false,
|
||||||
|
severity:"HIGH",
|
||||||
|
timestamp:$ts,
|
||||||
|
evidence:[{path:"constitution_hash.json"}],
|
||||||
|
details:{error:"constitution hash not collected"}
|
||||||
|
}')"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
OBSERVED="$(jq -r '.sha256 // empty' "$FILE")"
|
||||||
|
if [[ -z "$PINNED_SHA256" ]]; then
|
||||||
|
json_emit "$(jq -n --arg ts "$TS" --arg observed "$OBSERVED" '{
|
||||||
|
version:"1.0.0",
|
||||||
|
rule_id:"governance.constitution_pinned",
|
||||||
|
control_ids:["GV-01"],
|
||||||
|
passed:false,
|
||||||
|
severity:"HIGH",
|
||||||
|
timestamp:$ts,
|
||||||
|
evidence:[{path:"constitution_hash.json"}],
|
||||||
|
details:{error:"no pinned hash configured", observed_sha256:$observed}
|
||||||
|
}')"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$OBSERVED" == "$PINNED_SHA256" ]]; then
|
||||||
|
json_emit "$(jq -n --arg ts "$TS" '{
|
||||||
|
version:"1.0.0",
|
||||||
|
rule_id:"governance.constitution_pinned",
|
||||||
|
control_ids:["GV-01"],
|
||||||
|
passed:true,
|
||||||
|
severity:"HIGH",
|
||||||
|
timestamp:$ts,
|
||||||
|
evidence:[{path:"constitution_hash.json"}],
|
||||||
|
details:{}
|
||||||
|
}')"
|
||||||
|
else
|
||||||
|
json_emit "$(jq -n --arg ts "$TS" --arg observed "$OBSERVED" --arg pinned "$PINNED_SHA256" '{
|
||||||
|
version:"1.0.0",
|
||||||
|
rule_id:"governance.constitution_pinned",
|
||||||
|
control_ids:["GV-01"],
|
||||||
|
passed:false,
|
||||||
|
severity:"HIGH",
|
||||||
|
timestamp:$ts,
|
||||||
|
evidence:[{path:"constitution_hash.json"}],
|
||||||
|
details:{error:"hash mismatch", observed_sha256:$observed, pinned_sha256:$pinned}
|
||||||
|
}')"
|
||||||
|
fi
|
||||||
65
40-rules/ledger_hash_chain_intact.sh
Executable file
65
40-rules/ledger_hash_chain_intact.sh
Executable file
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../scripts/lib/common.sh"
|
||||||
|
require_cmd jq
|
||||||
|
|
||||||
|
EVID_DIR="${1:?usage: ledger_hash_chain_intact.sh <evidence_dir>}"
|
||||||
|
TS="$(iso_utc_now)"
|
||||||
|
|
||||||
|
FILE="$EVID_DIR/ledger_verify.json"
|
||||||
|
if [[ ! -f "$FILE" ]]; then
|
||||||
|
json_emit "$(jq -n --arg ts "$TS" '{
|
||||||
|
version:"1.0.0",
|
||||||
|
rule_id:"ledger.hash_chain_intact",
|
||||||
|
control_ids:["AU-01","AU-02"],
|
||||||
|
passed:false,
|
||||||
|
severity:"CRITICAL",
|
||||||
|
timestamp:$ts,
|
||||||
|
evidence:[{path:"ledger_verify.json"}],
|
||||||
|
details:{error:"missing evidence file"}
|
||||||
|
}')"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
COLLECTED="$(jq -r '.collected // true' "$FILE")"
|
||||||
|
if [[ "$COLLECTED" != "true" ]]; then
|
||||||
|
json_emit "$(jq -n --arg ts "$TS" '{
|
||||||
|
version:"1.0.0",
|
||||||
|
rule_id:"ledger.hash_chain_intact",
|
||||||
|
control_ids:["AU-01","AU-02"],
|
||||||
|
passed:false,
|
||||||
|
severity:"CRITICAL",
|
||||||
|
timestamp:$ts,
|
||||||
|
evidence:[{path:"ledger_verify.json"}],
|
||||||
|
details:{error:"ledger verify not collected"}
|
||||||
|
}')"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
OK="$(jq -r '.ok // false' "$FILE")"
|
||||||
|
ENTRY_COUNT="$(jq -r '.entry_count // 0' "$FILE")"
|
||||||
|
FAILURES_JSON="$(jq -c '.failures // []' "$FILE")"
|
||||||
|
|
||||||
|
if [[ "$OK" == "true" ]]; then
|
||||||
|
json_emit "$(jq -n --arg ts "$TS" --argjson count "$ENTRY_COUNT" '{
|
||||||
|
version:"1.0.0",
|
||||||
|
rule_id:"ledger.hash_chain_intact",
|
||||||
|
control_ids:["AU-01","AU-02"],
|
||||||
|
passed:true,
|
||||||
|
severity:"CRITICAL",
|
||||||
|
timestamp:$ts,
|
||||||
|
evidence:[{path:"ledger_verify.json"}],
|
||||||
|
details:{entries_checked:$count}
|
||||||
|
}')"
|
||||||
|
else
|
||||||
|
json_emit "$(jq -n --arg ts "$TS" --argjson count "$ENTRY_COUNT" --argjson failures "$FAILURES_JSON" '{
|
||||||
|
version:"1.0.0",
|
||||||
|
rule_id:"ledger.hash_chain_intact",
|
||||||
|
control_ids:["AU-01","AU-02"],
|
||||||
|
passed:false,
|
||||||
|
severity:"CRITICAL",
|
||||||
|
timestamp:$ts,
|
||||||
|
evidence:[{path:"ledger_verify.json"}],
|
||||||
|
details:{entries_checked:$count, failures:$failures}
|
||||||
|
}')"
|
||||||
|
fi
|
||||||
24
README.md
24
README.md
@@ -21,14 +21,30 @@ vm-cc is the continuous compliance and evidence orchestration layer. It ingests
|
|||||||
- 90-automation/: pipelines/glue for end-to-end runs
|
- 90-automation/: pipelines/glue for end-to-end runs
|
||||||
- scripts/: thin CLI wrappers to orchestrate collect → evaluate → report → sign
|
- scripts/: thin CLI wrappers to orchestrate collect → evaluate → report → sign
|
||||||
|
|
||||||
|
## Run directories
|
||||||
|
Each execution writes to a per-run folder set:
|
||||||
|
- 30-evidence/YYYY-MM-DD/RUN_ID/
|
||||||
|
- 50-reports/YYYY-MM-DD/RUN_ID/
|
||||||
|
- 60-proofs/YYYY-MM-DD/RUN_ID/
|
||||||
|
- 70-violations/YYYY-MM-DD/RUN_ID/
|
||||||
|
|
||||||
|
RUN_ID format: `YYYYMMDDThhmmssZ_<shorthash>`.
|
||||||
|
|
||||||
## Rule result contract (example)
|
## Rule result contract (example)
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
"rule_id": "authority-hierarchy",
|
"version": "1.0.0",
|
||||||
|
"rule_id": "ledger.hash_chain_intact",
|
||||||
|
"control_ids": ["AU-01", "AU-02"],
|
||||||
"passed": true,
|
"passed": true,
|
||||||
"severity": "HIGH",
|
"severity": "CRITICAL",
|
||||||
"timestamp": "2025-12-27T12:00:00Z",
|
"timestamp": "2025-12-27T12:00:00Z",
|
||||||
"evidence": ["30-evidence/2025-12-27/authority.json"],
|
"evidence": [
|
||||||
"details": { "checked_transitions": 42 }
|
{
|
||||||
|
"path": "30-evidence/2025-12-27/20251227T120000Z_ab12/ledger_verify.json",
|
||||||
|
"sha256": "..."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"details": { "entries_checked": 18231 }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
# Rule registry. Each entry binds a rule id to a source and evaluation script.
|
version: "1.0.0"
|
||||||
# Keep rule definitions in 40-rules/; this file ties them to schedules and severity.
|
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
- id: authority-hierarchy
|
- rule_id: "ledger.hash_chain_intact"
|
||||||
severity: HIGH
|
severity: "CRITICAL"
|
||||||
entry: "40-rules/authority_hierarchy.yaml"
|
script: "40-rules/ledger_hash_chain_intact.sh"
|
||||||
evidence:
|
evidence:
|
||||||
- vm_mcp
|
- "ledger_verify.json"
|
||||||
- vm_ledger
|
controls: ["AU-01", "AU-02"]
|
||||||
|
|
||||||
- id: skills-health
|
- rule_id: "governance.constitution_pinned"
|
||||||
severity: MEDIUM
|
severity: "HIGH"
|
||||||
entry: "40-rules/skills_health.yaml"
|
script: "40-rules/governance_constitution_pinned.sh"
|
||||||
evidence:
|
evidence:
|
||||||
- vm_skills
|
- "constitution_hash.json"
|
||||||
- ops
|
controls: ["GV-01"]
|
||||||
|
|
||||||
- id: receipts-schema
|
- rule_id: "backup.restore_drill_recent"
|
||||||
severity: HIGH
|
severity: "MEDIUM"
|
||||||
entry: "40-rules/receipts_schema.yaml"
|
script: "40-rules/backup_restore_drill_recent.sh"
|
||||||
evidence:
|
evidence:
|
||||||
- vm_contracts
|
- "backup_restore_drill.json"
|
||||||
- vm_ledger
|
controls: ["BC-01"]
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
# Schedules for rule execution. Cron-like or duration strings.
|
version: "1.0.0"
|
||||||
|
|
||||||
schedules:
|
schedules:
|
||||||
- name: hourly-critical
|
- name: "hourly"
|
||||||
every: "1h"
|
cadence: "0 * * * *"
|
||||||
rules:
|
run: ["collect", "evaluate", "report", "sign"]
|
||||||
- authority-hierarchy
|
|
||||||
- receipts-schema
|
|
||||||
|
|
||||||
- name: daily-health
|
- name: "daily"
|
||||||
every: "24h"
|
cadence: "0 20 * * *"
|
||||||
rules:
|
run: ["collect", "evaluate", "report", "sign", "anchor"]
|
||||||
- skills-health
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ vm_skills:
|
|||||||
- "*/outputs/ROOT.txt"
|
- "*/outputs/ROOT.txt"
|
||||||
|
|
||||||
ops:
|
ops:
|
||||||
path: "../ops"
|
path: "../vm-ops"
|
||||||
evidence:
|
evidence:
|
||||||
- "20-identity/**"
|
- "20-identity/**"
|
||||||
- "60-backups/**"
|
- "60-backups/**"
|
||||||
|
|||||||
38
schemas/rule_result.schema.json
Normal file
38
schemas/rule_result.schema.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"title": "vm-cc rule_result v1",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"version",
|
||||||
|
"rule_id",
|
||||||
|
"passed",
|
||||||
|
"severity",
|
||||||
|
"timestamp",
|
||||||
|
"evidence",
|
||||||
|
"details"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string" },
|
||||||
|
"rule_id": { "type": "string" },
|
||||||
|
"passed": { "type": "boolean" },
|
||||||
|
"severity": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["LOW", "MEDIUM", "HIGH", "CRITICAL"]
|
||||||
|
},
|
||||||
|
"timestamp": { "type": "string" },
|
||||||
|
"control_ids": { "type": "array", "items": { "type": "string" } },
|
||||||
|
"evidence": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["path"],
|
||||||
|
"properties": {
|
||||||
|
"path": { "type": "string" },
|
||||||
|
"sha256": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"details": { "type": "object" },
|
||||||
|
"remediation": { "type": ["object", "null"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
26
scripts/lib/common.sh
Executable file
26
scripts/lib/common.sh
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
vmcc_root() {
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd
|
||||||
|
}
|
||||||
|
|
||||||
|
iso_utc_now() {
|
||||||
|
date -u "+%Y-%m-%dT%H:%M:%SZ"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_id() {
|
||||||
|
local ts
|
||||||
|
ts="$(date -u "+%Y%m%dT%H%M%SZ")"
|
||||||
|
local rnd
|
||||||
|
rnd="$(head -c 8 /dev/urandom | od -An -tx1 | tr -d ' \n')"
|
||||||
|
echo "${ts}_${rnd:0:8}"
|
||||||
|
}
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || { echo "missing required tool: $1" >&2; exit 2; }
|
||||||
|
}
|
||||||
|
|
||||||
|
json_emit() {
|
||||||
|
printf '%s\n' "$1"
|
||||||
|
}
|
||||||
64
scripts/vmcc
Executable file
64
scripts/vmcc
Executable file
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(dirname "$0")/lib/common.sh"
|
||||||
|
|
||||||
|
ROOT="$(vmcc_root)"
|
||||||
|
CMD="${1:-}"
|
||||||
|
RUN_ID="${VMCC_RUN_ID:-$(run_id)}"
|
||||||
|
DAY="$(date -u "+%Y-%m-%d")"
|
||||||
|
|
||||||
|
EVID_DIR="$ROOT/30-evidence/$DAY/$RUN_ID"
|
||||||
|
RULE_DIR="$ROOT/50-reports/$DAY/$RUN_ID/rules"
|
||||||
|
REP_DIR="$ROOT/50-reports/$DAY/$RUN_ID"
|
||||||
|
|
||||||
|
mkdir -p "$EVID_DIR" "$RULE_DIR" "$REP_DIR"
|
||||||
|
|
||||||
|
case "$CMD" in
|
||||||
|
collect)
|
||||||
|
echo "[vmcc] run_id=$RUN_ID"
|
||||||
|
echo "[vmcc] collecting evidence -> $EVID_DIR"
|
||||||
|
"$ROOT/20-collectors/collect_ledger_verify.sh" "$EVID_DIR"
|
||||||
|
"$ROOT/20-collectors/collect_constitution_hash.sh" "$EVID_DIR"
|
||||||
|
"$ROOT/20-collectors/collect_backup_restore_drill.sh" "$EVID_DIR"
|
||||||
|
;;
|
||||||
|
evaluate)
|
||||||
|
echo "[vmcc] evaluating rules -> $RULE_DIR"
|
||||||
|
"$ROOT/40-rules/ledger_hash_chain_intact.sh" "$EVID_DIR" > "$RULE_DIR/ledger.hash_chain_intact.json"
|
||||||
|
"$ROOT/40-rules/governance_constitution_pinned.sh" "$EVID_DIR" > "$RULE_DIR/governance.constitution_pinned.json"
|
||||||
|
"$ROOT/40-rules/backup_restore_drill_recent.sh" "$EVID_DIR" > "$RULE_DIR/backup.restore_drill_recent.json"
|
||||||
|
;;
|
||||||
|
report)
|
||||||
|
require_cmd jq
|
||||||
|
echo "[vmcc] assembling report -> $REP_DIR/report.json"
|
||||||
|
TS="$(iso_utc_now)"
|
||||||
|
PASSED_COUNT="$(jq -s '[.[] | select(.passed==true)] | length' "$RULE_DIR"/*.json)"
|
||||||
|
FAILED_COUNT="$(jq -s '[.[] | select(.passed==false)] | length' "$RULE_DIR"/*.json)"
|
||||||
|
|
||||||
|
jq -n \
|
||||||
|
--arg version "1.0.0" \
|
||||||
|
--arg timestamp "$TS" \
|
||||||
|
--arg run_id "$RUN_ID" \
|
||||||
|
--arg day "$DAY" \
|
||||||
|
--slurpfile rules <(cat "$RULE_DIR"/*.json) \
|
||||||
|
--argjson passed "$PASSED_COUNT" \
|
||||||
|
--argjson failed "$FAILED_COUNT" \
|
||||||
|
'{
|
||||||
|
version: $version,
|
||||||
|
timestamp: $timestamp,
|
||||||
|
period: "run",
|
||||||
|
run_id: $run_id,
|
||||||
|
day: $day,
|
||||||
|
summary: {
|
||||||
|
rules_passed: $passed,
|
||||||
|
rules_failed: $failed,
|
||||||
|
status: (if $failed == 0 then "COMPLIANT" else "NONCOMPLIANT" end)
|
||||||
|
},
|
||||||
|
rules: $rules
|
||||||
|
}' > "$REP_DIR/report.json"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 {collect|evaluate|report}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user