Skip to content

Commit 629f655

Browse files
SecAI-Hubclaude
andcommitted
Integrate gguf-guard deep GGUF integrity scanner across SecAI_OS (M30)
Build system compiles gguf-guard from source or clones from GitHub. Quarantine pipeline runs gguf-guard scan as Stage 5 scanner #7 for GGUF files, generates per-tensor SHA-256 manifests and structural fingerprints on promotion. Registry stores fingerprint/manifest metadata and exposes /v1/model/verify-manifest endpoint. UI displays gguf-guard metadata on model cards with a "Verify Tensors" button. Graceful degradation when gguf-guard is not installed (fail-open unless policy requires it). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 153d120 commit 629f655

11 files changed

Lines changed: 446 additions & 6 deletions

File tree

files/scripts/build-services.sh

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ SRC_DIR="/tmp/secure-ai-build"
1111
echo "=== Building Secure AI services ==="
1212

1313
# Install build dependencies
14-
dnf install -y golang python3 python3-pip 2>/dev/null || true
14+
dnf install -y golang python3 python3-pip cmake gcc gcc-c++ 2>/dev/null || true
1515

1616
mkdir -p "$INSTALL_DIR" "$SRC_DIR"
1717

@@ -30,6 +30,35 @@ cd "${SRC_DIR}/registry"
3030
CGO_ENABLED=0 go build -ldflags="-s -w" -o /usr/local/bin/securectl ./cmd/securectl/
3131
echo " -> /usr/local/bin/securectl"
3232

33+
# --- gguf-guard (GGUF model integrity scanner) ---
34+
echo "Building: gguf-guard"
35+
if [ -d "/tmp/gguf-guard" ]; then
36+
cp -r /tmp/gguf-guard "${SRC_DIR}/gguf-guard"
37+
else
38+
git clone --depth 1 https://github.com/SecAI-Hub/gguf-guard.git "${SRC_DIR}/gguf-guard" 2>/dev/null || \
39+
echo "WARNING: gguf-guard clone failed — GGUF integrity scanner will not be available"
40+
fi
41+
if [ -d "${SRC_DIR}/gguf-guard" ]; then
42+
cd "${SRC_DIR}/gguf-guard"
43+
CGO_ENABLED=0 go build -ldflags="-s -w" -o /usr/local/bin/gguf-guard ./cmd/gguf-guard/
44+
echo " -> /usr/local/bin/gguf-guard"
45+
fi
46+
47+
# --- llama.cpp (inference engine) ---
48+
echo "Building: llama-server"
49+
LLAMA_CPP_VERSION="${LLAMA_CPP_VERSION:-b5200}"
50+
cd "$SRC_DIR"
51+
curl -fsSL "https://github.com/ggml-org/llama.cpp/archive/refs/tags/${LLAMA_CPP_VERSION}.tar.gz" \
52+
| tar xz
53+
cd "llama.cpp-${LLAMA_CPP_VERSION#b}"
54+
cmake -B build -DGGML_CUDA=ON -DGGML_VULKAN=ON -DBUILD_SHARED_LIBS=OFF \
55+
-DCMAKE_BUILD_TYPE=Release 2>/dev/null || \
56+
cmake -B build -DGGML_VULKAN=ON -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release 2>/dev/null || \
57+
cmake -B build -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release
58+
cmake --build build --target llama-server -j"$(nproc)"
59+
install -m 755 build/bin/llama-server /usr/bin/llama-server
60+
echo " -> /usr/bin/llama-server"
61+
3362
# --- Python services (installed as wrapper scripts) ---
3463

3564
# Quarantine watcher
@@ -44,6 +73,15 @@ WRAPPER
4473
chmod +x "${INSTALL_DIR}/quarantine-watcher"
4574
echo " -> ${INSTALL_DIR}/quarantine-watcher"
4675

76+
# Quarantine scanning tools (installed independently so one failure doesn't block others)
77+
echo "Installing: quarantine scanning tools"
78+
for scanner in modelscan fickling garak modelaudit; do
79+
echo " Installing: ${scanner}"
80+
pip3 install --prefix=/usr --no-cache-dir "${scanner}" 2>/dev/null || \
81+
pip3 install --prefix=/usr --break-system-packages --no-cache-dir "${scanner}" 2>/dev/null || \
82+
echo " WARNING: ${scanner} install failed — scanner will be skipped at runtime"
83+
done
84+
4785
# Web UI
4886
echo "Building: ui"
4987
pip3 install --prefix=/usr --no-cache-dir /tmp/services/ui 2>/dev/null || \
@@ -78,6 +116,12 @@ WRAPPER
78116
chmod +x "${INSTALL_DIR}/search-mediator"
79117
echo " -> ${INSTALL_DIR}/search-mediator"
80118

119+
# HuggingFace CLI (for model downloads)
120+
echo "Installing: huggingface-hub"
121+
pip3 install --prefix=/usr --no-cache-dir huggingface-hub 2>/dev/null || \
122+
pip3 install --prefix=/usr --break-system-packages --no-cache-dir huggingface-hub 2>/dev/null || \
123+
echo "WARNING: huggingface-hub install failed — model downloads will use git clone fallback"
124+
81125
# Install SearXNG via pip if not available as RPM
82126
echo "Installing: searxng"
83127
pip3 install --prefix=/usr --no-cache-dir searxng 2>/dev/null || \
@@ -86,7 +130,7 @@ pip3 install --prefix=/usr --no-cache-dir searxng 2>/dev/null || \
86130

87131
# Cleanup build artifacts
88132
rm -rf "$SRC_DIR"
89-
dnf remove -y golang 2>/dev/null || true
133+
dnf remove -y golang cmake gcc gcc-c++ 2>/dev/null || true
90134
dnf clean all 2>/dev/null || true
91135

92136
echo "=== Secure AI services installed ==="

files/system/etc/secure-ai/policy/policy.yaml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,29 @@ quarantine:
2424
format_gate: true # Stage 2: validate headers + reject unsafe formats
2525
integrity_check: true # Stage 3: hash pinning verification
2626
provenance_check: true # Stage 4: cosign / signature verification
27-
static_scan: true # Stage 5: modelscan + entropy analysis
27+
static_scan: true # Stage 5: modelscan + entropy + gguf-guard
2828
behavioral_test: true # Stage 6: adversarial prompt suite (LLM only)
2929
diffusion_deep_scan: true # Stage 7: config integrity (diffusion only)
3030
# Smoke test threshold: fail if >30% prompts flagged OR >1 critical flag
3131
smoke_test_max_score: 0.3
3232
smoke_test_max_critical: 1
3333

34+
# gguf-guard: GGUF model integrity and anomaly scanner
35+
# Provides deep weight-level analysis beyond modelscan/fickling:
36+
# - Layered anomaly scoring (per-tensor, cross-layer, model-global, reference)
37+
# - Quant-format-aware block analysis (scale entropy, repeated blocks)
38+
# - Per-tensor SHA-256 integrity manifests with Merkle tree
39+
# - Structural policy validation and model family identification
40+
gguf_guard:
41+
# Whether gguf-guard is required (fail-closed if not installed)
42+
required: false
43+
# Generate per-tensor integrity manifest on promotion
44+
generate_manifest: true
45+
# Generate structural fingerprint on promotion
46+
generate_fingerprint: true
47+
# Verify manifest on periodic integrity checks (complements fs-verity)
48+
verify_on_integrity_check: true
49+
3450
tools:
3551
default: "deny"
3652
rate_limit:

recipes/recipe.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,15 @@ modules:
3030
- vulkan-tools # vulkaninfo for diagnostics
3131
- libdrm # DRM library (all GPUs)
3232
- clinfo # OpenCL diagnostics
33+
# Model download tools
34+
- git # Git for HuggingFace model cloning
35+
# Clipboard auto-clear (M21)
36+
- wl-clipboard # wl-copy/wl-paste for Wayland clipboard clearing
3337
# Tor + SearXNG (anonymous web search)
3438
- tor # Tor SOCKS5 proxy
3539
- python3-searxng # SearXNG metasearch (or installed via pip)
40+
# Model integrity (M27)
41+
- fsverity-utils # fs-verity Merkle tree integrity on model files
3642
# Canary / tripwire inotify watcher (M22)
3743
- inotify-tools # inotifywait for real-time file monitoring
3844
# Secure Boot + TPM2 (M17)

services/quarantine/Containerfile

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@ COPY requirements.lock .
66
COPY quarantine/ quarantine/
77

88
RUN pip install --no-cache-dir --require-hashes -r requirements.lock && \
9-
pip install --no-cache-dir --no-deps . && \
10-
pip install --no-cache-dir modelscan || true
9+
pip install --no-cache-dir --no-deps .
10+
11+
# Install scanning tools — each independently so one failure doesn't block others
12+
RUN pip install --no-cache-dir modelscan || echo "WARN: modelscan not available"
13+
RUN pip install --no-cache-dir fickling || echo "WARN: fickling not available"
14+
RUN pip install --no-cache-dir garak || echo "WARN: garak not available"
15+
RUN pip install --no-cache-dir modelaudit || echo "WARN: modelaudit not available"
1116

1217
USER 65534:65534
1318
ENTRYPOINT ["secure-ai-quarantine"]

services/quarantine/pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ dependencies = [
1111
[project.optional-dependencies]
1212
scan = [
1313
"modelscan>=0.8",
14+
"fickling>=0.1",
15+
"garak>=0.9",
1416
]
1517

1618
[project.scripts]

services/quarantine/quarantine/pipeline.py

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
os.getenv("SOURCES_ALLOWLIST_PATH", "/etc/secure-ai/policy/sources.allowlist.yaml")
4646
)
4747
LLAMA_SERVER_BIN = os.getenv("LLAMA_SERVER_BIN", "/usr/bin/llama-server")
48+
GGUF_GUARD_BIN = os.getenv("GGUF_GUARD_BIN", "/usr/local/bin/gguf-guard")
4849
SMOKE_TEST_TIMEOUT = int(os.getenv("SMOKE_TEST_TIMEOUT", "120"))
4950

5051

@@ -961,8 +962,130 @@ def _check_weight_anomalies(tensor_name: str, stats: dict) -> list:
961962
return issues
962963

963964

965+
def _run_gguf_guard_scan(artifact_path: Path, policy: dict | None = None,
966+
reference_path: str | None = None) -> dict:
967+
"""Run gguf-guard static analysis on a GGUF model file.
968+
969+
gguf-guard provides deep weight-level anomaly detection including:
970+
- Layered anomaly scoring (tensor-local, cross-layer, model-global, reference)
971+
- Quant-format-aware block analysis (scale entropy, repeated blocks, saturation)
972+
- Robust statistics (median/MAD, trimmed mean, Tukey fences)
973+
- Structural policy validation (offsets, overlaps, metadata, tensor shapes)
974+
- Model family identification (llama, mistral, mixtral, qwen2, gemma, phi)
975+
976+
Returns scan result with score, anomalies, and pass/fail verdict.
977+
"""
978+
if artifact_path.suffix.lower() != ".gguf":
979+
return {"passed": True, "scanner": "gguf-guard", "note": "not a GGUF file, skipped"}
980+
981+
if policy is None:
982+
policy = {}
983+
gguf_guard_policy = policy.get("gguf_guard", {})
984+
985+
try:
986+
cmd = [GGUF_GUARD_BIN, "scan", "--quiet"]
987+
if reference_path:
988+
cmd.extend(["--reference", reference_path])
989+
cmd.append(str(artifact_path))
990+
991+
result = subprocess.run(
992+
cmd, capture_output=True, text=True, timeout=600,
993+
)
994+
995+
output = result.stdout.strip()
996+
997+
if result.returncode == 0:
998+
# PASS
999+
return {
1000+
"passed": True,
1001+
"scanner": "gguf-guard",
1002+
"output": output,
1003+
"exit_code": 0,
1004+
}
1005+
elif result.returncode == 2:
1006+
# FAIL — score exceeded threshold
1007+
return {
1008+
"passed": False,
1009+
"scanner": "gguf-guard",
1010+
"reason": f"gguf-guard scan failed: {output}",
1011+
"output": output,
1012+
"exit_code": 2,
1013+
}
1014+
else:
1015+
# Error
1016+
log.warning("gguf-guard error (exit %d): %s", result.returncode, result.stderr[:500])
1017+
return {
1018+
"passed": True,
1019+
"scanner": "gguf-guard",
1020+
"note": f"gguf-guard error (exit {result.returncode}), non-fatal",
1021+
"exit_code": result.returncode,
1022+
}
1023+
1024+
except FileNotFoundError:
1025+
require = gguf_guard_policy.get("required", False)
1026+
if require:
1027+
return {"passed": False, "scanner": "gguf-guard", "reason": "gguf-guard required but not installed"}
1028+
log.info("gguf-guard not installed; skipping GGUF integrity scan")
1029+
return {"passed": True, "scanner": "gguf-guard", "note": "not installed, skipped"}
1030+
except subprocess.TimeoutExpired:
1031+
log.warning("gguf-guard timed out after 600s")
1032+
return {"passed": False, "scanner": "gguf-guard", "reason": "gguf-guard scan timed out"}
1033+
except Exception as e:
1034+
log.warning("gguf-guard error: %s", e)
1035+
return {"passed": True, "scanner": "gguf-guard", "note": f"error (non-fatal): {e}"}
1036+
1037+
1038+
def _run_gguf_guard_manifest(artifact_path: Path, output_path: Path) -> dict:
1039+
"""Generate a gguf-guard per-tensor integrity manifest for a GGUF file.
1040+
1041+
The manifest contains SHA-256 hashes for each tensor and a Merkle tree root,
1042+
enabling fine-grained integrity verification at any time.
1043+
"""
1044+
if artifact_path.suffix.lower() != ".gguf":
1045+
return {"generated": False, "note": "not a GGUF file"}
1046+
1047+
try:
1048+
result = subprocess.run(
1049+
[GGUF_GUARD_BIN, "manifest", "--output", str(output_path), str(artifact_path)],
1050+
capture_output=True, text=True, timeout=600,
1051+
)
1052+
if result.returncode == 0:
1053+
return {"generated": True, "manifest_path": str(output_path)}
1054+
else:
1055+
log.warning("gguf-guard manifest generation failed: %s", result.stderr[:500])
1056+
return {"generated": False, "error": result.stderr[:200]}
1057+
except FileNotFoundError:
1058+
return {"generated": False, "note": "gguf-guard not installed"}
1059+
except Exception as e:
1060+
log.warning("gguf-guard manifest error: %s", e)
1061+
return {"generated": False, "error": str(e)}
1062+
1063+
1064+
def _run_gguf_guard_fingerprint(artifact_path: Path) -> dict | None:
1065+
"""Generate a gguf-guard structural fingerprint for a GGUF file.
1066+
1067+
Returns fingerprint dict (file_hash, structure_hash, quant_type, etc.) or None.
1068+
"""
1069+
if artifact_path.suffix.lower() != ".gguf":
1070+
return None
1071+
1072+
try:
1073+
result = subprocess.run(
1074+
[GGUF_GUARD_BIN, "fingerprint", str(artifact_path)],
1075+
capture_output=True, text=True, timeout=120,
1076+
)
1077+
if result.returncode == 0:
1078+
return json.loads(result.stdout)
1079+
return None
1080+
except (FileNotFoundError, json.JSONDecodeError, subprocess.TimeoutExpired):
1081+
return None
1082+
except Exception as e:
1083+
log.warning("gguf-guard fingerprint error: %s", e)
1084+
return None
1085+
1086+
9641087
def check_static_scan(artifact_path: Path, policy: dict | None = None) -> dict:
965-
"""Stage 5: Run modelscan + fickling + modelaudit + entropy + weight analysis."""
1088+
"""Stage 5: Run modelscan + fickling + modelaudit + entropy + weight analysis + gguf-guard."""
9661089
if policy is None:
9671090
policy = {}
9681091
results = {}
@@ -991,6 +1114,10 @@ def check_static_scan(artifact_path: Path, policy: dict | None = None) -> dict:
9911114
weight_result = _analyze_weight_distribution(artifact_path)
9921115
results["weight_stats"] = weight_result
9931116

1117+
# 7. gguf-guard deep integrity scan (GGUF files only)
1118+
gguf_guard_result = _run_gguf_guard_scan(artifact_path, policy=policy)
1119+
results["gguf_guard"] = gguf_guard_result
1120+
9941121
# Overall: fail if ANY scanner fails
9951122
failed = [k for k, v in results.items() if not v.get("passed", True)]
9961123
if failed:
@@ -1704,6 +1831,18 @@ def run_pipeline(artifact_path: Path, file_hash: str, policy: dict,
17041831
else:
17051832
details["smoke_test"] = {"passed": True, "note": "not applicable for safetensors"}
17061833

1834+
# Post-scan: generate gguf-guard artifacts for promotion metadata
1835+
if artifact_path.suffix.lower() == ".gguf":
1836+
# Structural fingerprint (stored in promotion metadata)
1837+
fp = _run_gguf_guard_fingerprint(artifact_path)
1838+
if fp:
1839+
details["gguf_guard_fingerprint"] = fp
1840+
1841+
# Per-tensor integrity manifest (stored alongside model in registry)
1842+
manifest_path = artifact_path.with_suffix(".gguf.manifest.json")
1843+
manifest_result = _run_gguf_guard_manifest(artifact_path, manifest_path)
1844+
details["gguf_guard_manifest"] = manifest_result
1845+
17071846
return {"passed": True, "reason": "all_checks_passed", "details": details}
17081847

17091848

services/quarantine/quarantine/watcher.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,15 @@ def promote_to_registry(filename: str, file_hash: str, size_bytes: int,
159159
"policy_version": _compute_policy_version(),
160160
}
161161

162+
# Include gguf-guard data if available from pipeline
163+
if pipeline_details:
164+
fp = pipeline_details.get("gguf_guard_fingerprint")
165+
if fp:
166+
payload["gguf_guard_fingerprint"] = fp
167+
manifest_info = pipeline_details.get("gguf_guard_manifest", {})
168+
if manifest_info.get("generated"):
169+
payload["gguf_guard_manifest"] = manifest_info.get("manifest_path", "")
170+
162171
try:
163172
req = Request(
164173
f"{REGISTRY_URL}/v1/model/promote",

0 commit comments

Comments
 (0)