Skip to content

Commit cfe409e

Browse files
committed
Harden linked service integrations
1 parent 2637e09 commit cfe409e

10 files changed

Lines changed: 254 additions & 29 deletions

File tree

services/gpu-integrity-watch/actions.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"time"
1313
)
1414

15+
var outboundHTTPClient = &http.Client{Timeout: 15 * time.Second}
16+
1517
// ActionType identifies a response action.
1618
type ActionType string
1719

@@ -34,19 +36,19 @@ type ActionConfig struct {
3436

3537
// ActionResult records the outcome of an executed action.
3638
type ActionResult struct {
37-
Action string `json:"action"`
39+
Action string `json:"action"`
3840
Type ActionType `json:"type"`
39-
Triggered bool `json:"triggered"`
40-
Success bool `json:"success"`
41-
Message string `json:"message"`
42-
Timestamp time.Time `json:"timestamp"`
41+
Triggered bool `json:"triggered"`
42+
Success bool `json:"success"`
43+
Message string `json:"message"`
44+
Timestamp time.Time `json:"timestamp"`
4345
}
4446

4547
// ActionExecutor evaluates scoring results and triggers configured actions.
4648
type ActionExecutor struct {
47-
actions []ActionConfig
48-
modelDir string
49-
inferURL string
49+
actions []ActionConfig
50+
modelDir string
51+
inferURL string
5052
}
5153

5254
// NewActionExecutor creates an executor with the given action configs.
@@ -118,7 +120,7 @@ func (e *ActionExecutor) executeReload(ac ActionConfig) ActionResult {
118120

119121
// Try llama.cpp-style reload endpoint
120122
url := strings.TrimSuffix(target, "/") + "/reload"
121-
resp, err := http.Post(url, "application/json", strings.NewReader("{}"))
123+
resp, err := outboundHTTPClient.Post(url, "application/json", strings.NewReader("{}"))
122124
if err != nil {
123125
// Fall back to command if configured
124126
if ac.Command != "" {
@@ -210,7 +212,7 @@ func (e *ActionExecutor) executeAlert(ac ActionConfig, entry ScoreEntry) ActionR
210212

211213
if ac.Webhook != "" {
212214
body, _ := json.Marshal(payload)
213-
resp, err := http.Post(ac.Webhook, "application/json", strings.NewReader(string(body)))
215+
resp, err := outboundHTTPClient.Post(ac.Webhook, "application/json", strings.NewReader(string(body)))
214216
if err != nil {
215217
ar.Success = false
216218
ar.Message = fmt.Sprintf("webhook failed: %v", err)
@@ -250,7 +252,7 @@ func (e *ActionExecutor) executeFailClosed(ac ActionConfig) ActionResult {
250252
// Try to signal inference server shutdown
251253
if e.inferURL != "" {
252254
url := strings.TrimSuffix(e.inferURL, "/") + "/shutdown"
253-
resp, err := http.Post(url, "application/json", strings.NewReader("{}"))
255+
resp, err := outboundHTTPClient.Post(url, "application/json", strings.NewReader("{}"))
254256
if err != nil {
255257
ar.Success = false
256258
ar.Message = fmt.Sprintf("fail-closed shutdown request failed: %v", err)
@@ -271,6 +273,12 @@ func (e *ActionExecutor) executeFailClosed(ac ActionConfig) ActionResult {
271273
func executeCommand(ac ActionConfig) ActionResult {
272274
ar := ActionResult{Action: ac.Name, Type: ac.Type, Triggered: true}
273275

276+
if os.Getenv("GPU_WATCH_ALLOW_ACTION_COMMANDS") != "true" {
277+
ar.Success = false
278+
ar.Message = "command actions disabled; set GPU_WATCH_ALLOW_ACTION_COMMANDS=true"
279+
return ar
280+
}
281+
274282
cmd := exec.Command("sh", "-c", ac.Command)
275283
output, err := cmd.CombinedOutput()
276284
if err != nil {

services/gpu-integrity-watch/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ func cmdStatus() {
340340
}
341341
}
342342

343-
resp, err := http.Get(addr + "/v1/status")
343+
resp, err := outboundHTTPClient.Get(addr + "/v1/status")
344344
if err != nil {
345345
log.Fatalf("cannot reach daemon: %v", err)
346346
}

services/gpu-integrity-watch/probes.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"encoding/hex"
66
"fmt"
77
"io"
8-
"net/http"
98
"os"
109
"os/exec"
1110
"path/filepath"
@@ -369,7 +368,7 @@ func (r *ProbeRunner) runSentinelInference(pc ProbeConfig) ProbeResult {
369368
// querySentinel sends a completion request to the inference endpoint.
370369
func querySentinel(endpoint, input string) (string, error) {
371370
payload := fmt.Sprintf(`{"prompt":%q,"n_predict":64,"temperature":0}`, input)
372-
resp, err := http.Post(endpoint+"/completion", "application/json", strings.NewReader(payload))
371+
resp, err := outboundHTTPClient.Post(endpoint+"/completion", "application/json", strings.NewReader(payload))
373372
if err != nil {
374373
return "", err
375374
}

services/quarantine/quarantine/pipeline.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,22 @@
3333
import time
3434
from pathlib import Path
3535
from urllib.error import URLError
36+
from urllib.parse import urlparse
3637
from urllib.request import Request, urlopen
3738

3839
import yaml
3940

4041
log = logging.getLogger("quarantine.pipeline")
4142

43+
44+
def _http_urlopen(target, timeout: int = 30):
45+
"""Open only HTTP(S) URLs for scanner-local service calls."""
46+
raw_url = target.full_url if isinstance(target, Request) else str(target)
47+
scheme = urlparse(raw_url).scheme.lower()
48+
if scheme not in {"http", "https"}:
49+
raise URLError(f"unsupported URL scheme: {scheme or 'none'}")
50+
return urlopen(target, timeout=timeout) # nosec B310
51+
4252
MODELS_LOCK_PATH = Path(
4353
os.getenv("MODELS_LOCK_PATH", "/etc/secure-ai/policy/models.lock.yaml")
4454
)
@@ -1564,7 +1574,7 @@ def _wait_for_server(port: int, timeout: int = 30) -> bool:
15641574
while time.monotonic() < deadline:
15651575
for url in readiness_urls:
15661576
try:
1567-
with urlopen(url, timeout=2) as resp:
1577+
with _http_urlopen(url, timeout=2) as resp:
15681578
if getattr(resp, "status", 200) == 200:
15691579
return True
15701580
except (URLError, OSError):
@@ -1596,7 +1606,7 @@ def _query_llama(port: int, prompt_messages: list, timeout: int = 60) -> dict:
15961606
method="POST",
15971607
)
15981608
try:
1599-
with urlopen(req, timeout=timeout) as resp:
1609+
with _http_urlopen(req, timeout=timeout) as resp:
16001610
data = json.loads(resp.read())
16011611
return {
16021612
"ok": True,

services/quarantine/quarantine/watcher.py

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from datetime import datetime, timezone
2424
from pathlib import Path
2525
from urllib.error import URLError
26+
from urllib.parse import urlparse
2627
from urllib.request import Request, urlopen
2728

2829
import sys
@@ -39,7 +40,7 @@
3940
if _services_root not in sys.path:
4041
sys.path.insert(0, _services_root)
4142

42-
from common.audit_chain import AuditChain
43+
from common.audit_chain import AuditChain # noqa: E402
4344

4445
log = logging.getLogger("quarantine")
4546

@@ -176,6 +177,37 @@ def _policy_version_id() -> str:
176177
return str(info)
177178

178179

180+
def _stage_gguf_guard_manifest(pipeline_details: dict | None) -> None:
181+
"""Move a generated GGUF guard manifest into the registry directory."""
182+
if not pipeline_details:
183+
return
184+
manifest_info = pipeline_details.get("gguf_guard_manifest", {})
185+
if not isinstance(manifest_info, dict) or not manifest_info.get("generated"):
186+
return
187+
manifest_path = manifest_info.get("manifest_path")
188+
if not manifest_path:
189+
return
190+
191+
source = Path(manifest_path)
192+
dest = REGISTRY_DIR / source.name
193+
try:
194+
if source.resolve() != dest.resolve():
195+
if not source.exists():
196+
log.warning("gguf-guard manifest missing before registry promotion: %s", source)
197+
manifest_info["generated"] = False
198+
manifest_info["manifest_path"] = ""
199+
return
200+
shutil.move(str(source), str(dest))
201+
log.info("moved gguf-guard manifest to registry dir: %s", dest.name)
202+
except OSError as e:
203+
log.warning("could not stage gguf-guard manifest %s: %s", source, e)
204+
manifest_info["generated"] = False
205+
manifest_info["manifest_path"] = ""
206+
return
207+
208+
manifest_info["manifest_path"] = dest.name
209+
210+
179211
def _service_headers() -> dict[str, str]:
180212
"""Return inter-service auth headers when a token is configured."""
181213
try:
@@ -188,6 +220,15 @@ def _service_headers() -> dict[str, str]:
188220
return headers
189221

190222

223+
def _http_urlopen(target, timeout: int = 30):
224+
"""Open only HTTP(S) URLs for registry service calls."""
225+
raw_url = target.full_url if isinstance(target, Request) else str(target)
226+
scheme = urlparse(raw_url).scheme.lower()
227+
if scheme not in {"http", "https"}:
228+
raise URLError(f"unsupported URL scheme: {scheme or 'none'}")
229+
return urlopen(target, timeout=timeout) # nosec B310
230+
231+
191232
def promote_to_registry(filename: str, file_hash: str, size_bytes: int,
192233
scan_results: dict, model_type: str = "llm",
193234
source_url: str = "",
@@ -227,8 +268,9 @@ def promote_to_registry(filename: str, file_hash: str, size_bytes: int,
227268
if fp:
228269
payload["gguf_guard_fingerprint"] = fp
229270
manifest_info = pipeline_details.get("gguf_guard_manifest", {})
230-
if manifest_info.get("generated"):
231-
payload["gguf_guard_manifest"] = manifest_info.get("manifest_path", "")
271+
manifest_path = manifest_info.get("manifest_path", "")
272+
if manifest_info.get("generated") and manifest_path:
273+
payload["gguf_guard_manifest"] = Path(manifest_path).name
232274

233275
try:
234276
req = Request(
@@ -237,7 +279,7 @@ def promote_to_registry(filename: str, file_hash: str, size_bytes: int,
237279
headers=_service_headers(),
238280
method="POST",
239281
)
240-
with urlopen(req, timeout=30) as resp:
282+
with _http_urlopen(req, timeout=30) as resp:
241283
result = json.loads(resp.read())
242284
log.info("registry promotion response: %s", result)
243285
return resp.status == 201
@@ -304,6 +346,7 @@ def process_artifact(artifact_path: Path) -> bool:
304346

305347
# Collect scan result summary
306348
details = result.get("details", {})
349+
_stage_gguf_guard_manifest(details)
307350
scan_summary = _build_scan_summary(details)
308351

309352
# Extract source revision
@@ -410,6 +453,7 @@ def process_directory(artifact_dir: Path) -> bool:
410453
)
411454

412455
details = result.get("details", {})
456+
_stage_gguf_guard_manifest(details)
413457
scan_summary = _build_scan_summary(details)
414458
scan_summary["model_type"] = "diffusion"
415459

services/registry/cmd/securectl/main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import (
99
"os"
1010
"strings"
1111
"text/tabwriter"
12+
"time"
1213
)
1314

1415
var registryURL = "http://127.0.0.1:8470"
16+
var apiClient = &http.Client{Timeout: 15 * time.Second}
1517

1618
func init() {
1719
if u := os.Getenv("REGISTRY_URL"); u != "" {
@@ -61,7 +63,7 @@ func apiRequest(method, path string, body io.Reader) ([]byte, int, error) {
6163
req.Header.Set("Authorization", "Bearer "+token)
6264
}
6365

64-
resp, err := http.DefaultClient.Do(req)
66+
resp, err := apiClient.Do(req)
6567
if err != nil {
6668
return nil, 0, err
6769
}

0 commit comments

Comments
 (0)