Skip to content

Commit dc26188

Browse files
fix: address critical security and bug issues from codebase audit
Security fixes: - sandbox: fix path traversal via pathlib.Path.resolve() (#180) - sandbox: detect importlib.import_module() bypass in AST visitor (#181) - PromptInjectionDetector: add regex timeout + pattern length guard (#143) - hypervisor: tighten DID validation regex, remove @ (#163) - hypervisor: validate provider is a class after ep.load() (#164) - hypervisor: validate IATP trust score is finite and in [0,100] (#169) Bug fixes: - hypervisor: fix dead code - RingEngine -> RingEnforcer fallback (#165) - hypervisor: bound VFS edit log with deque(maxlen=10000) (#167) - agent-mesh: fix deprecated datetime.utcnow() -> now(timezone.utc) (#177) - agent-sre: bound ErrorBudget._events with deque, use monotonic time (#173,#174) - agent-sre: fix CascadeDetector.get_breaker() KeyError (#175) - PolicyRule: quote-aware SplitCompound() (#146) - CircuitBreaker: guard against arithmetic overflow (#142) - agent-compliance: handle corrupted manifest JSON (#153) - agent-compliance: replace bare except anti-pattern (#154) - agent-compliance: add UTF-8 encoding to file operations (#155) - agent-compliance: add CLI error handling (#156) - agent-compliance: use spec.get() for key validation (#157) CI fixes: - Remove continue-on-error on lint step (#182) - Remove silent test failure fallback (#183) - Add agent-compliance to test matrix (#184) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ac360ed commit dc26188

File tree

15 files changed

+174
-67
lines changed

15 files changed

+174
-67
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,13 @@ jobs:
2424
run: pip install --require-hashes --no-cache-dir -r requirements/ci-lint.txt
2525
- name: Lint ${{ matrix.package }}
2626
run: ruff check packages/${{ matrix.package }}/src/ --select E,F,W --ignore E501
27-
continue-on-error: true
2827

2928
test:
3029
runs-on: ubuntu-latest
3130
strategy:
3231
fail-fast: false
3332
matrix:
34-
package: [agent-os, agent-mesh, agent-hypervisor, agent-sre]
33+
package: [agent-os, agent-mesh, agent-hypervisor, agent-sre, agent-compliance]
3534
python-version: ["3.11", "3.12"]
3635
include:
3736
- package: agent-os
@@ -53,7 +52,7 @@ jobs:
5352
2>/dev/null || pip install --no-cache-dir pytest==8.4.1 pytest-asyncio==1.1.0 2>/dev/null || true
5453
- name: Test ${{ matrix.package }}
5554
working-directory: packages/${{ matrix.package }}
56-
run: pytest tests/ -x -q --tb=short 2>/dev/null || echo "No tests found"
55+
run: pytest tests/ -x -q --tb=short
5756

5857
security:
5958
runs-on: ubuntu-latest

packages/agent-compliance/src/agent_compliance/cli/main.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,25 +36,29 @@ def cmd_integrity(args: argparse.Namespace) -> int:
3636
"""Run integrity verification or generate manifest."""
3737
from agent_compliance.integrity import IntegrityVerifier
3838

39-
if args.generate:
40-
verifier = IntegrityVerifier()
41-
manifest = verifier.generate_manifest(args.generate)
42-
print(f"Manifest written to {args.generate}")
43-
print(f" Files hashed: {len(manifest['files'])}")
44-
print(f" Functions hashed: {len(manifest['functions'])}")
45-
return 0
46-
47-
verifier = IntegrityVerifier(manifest_path=args.manifest)
48-
report = verifier.verify()
49-
50-
if args.json:
51-
import json
52-
53-
print(json.dumps(report.to_dict(), indent=2))
54-
else:
55-
print(report.summary())
56-
57-
return 0 if report.passed else 1
39+
try:
40+
if args.generate:
41+
verifier = IntegrityVerifier()
42+
manifest = verifier.generate_manifest(args.generate)
43+
print(f"Manifest written to {args.generate}")
44+
print(f" Files hashed: {len(manifest['files'])}")
45+
print(f" Functions hashed: {len(manifest['functions'])}")
46+
return 0
47+
48+
verifier = IntegrityVerifier(manifest_path=args.manifest)
49+
report = verifier.verify()
50+
51+
if args.json:
52+
import json
53+
54+
print(json.dumps(report.to_dict(), indent=2))
55+
else:
56+
print(report.summary())
57+
58+
return 0 if report.passed else 1
59+
except Exception as e:
60+
print(f"Error: {e}", file=sys.stderr)
61+
return 1
5862

5963

6064
def main() -> int:

packages/agent-compliance/src/agent_compliance/integrity.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,14 @@ def __init__(
223223
self._manifest: Optional[dict] = None
224224

225225
if manifest_path and os.path.exists(manifest_path):
226-
with open(manifest_path) as f:
227-
self._manifest = json.load(f)
226+
try:
227+
with open(manifest_path, encoding="utf-8") as f:
228+
self._manifest = json.load(f)
229+
except json.JSONDecodeError as e:
230+
import logging
231+
logging.getLogger(__name__).warning(
232+
"Corrupted manifest at %s: %s", manifest_path, e
233+
)
228234

229235
def verify(self) -> IntegrityReport:
230236
"""Run full integrity verification.
@@ -358,7 +364,7 @@ def generate_manifest(self, output_path: str) -> dict:
358364
"sha256": _hash_file(source_file),
359365
"path": source_file,
360366
}
361-
except (ImportError, Exception) as e:
367+
except (ImportError, OSError, TypeError) as e:
362368
logger.warning("Could not hash module %s: %s", mod_name, e)
363369

364370
for mod_name, func_path in self.critical_functions:
@@ -368,12 +374,12 @@ def generate_manifest(self, output_path: str) -> dict:
368374
if func:
369375
key = f"{mod_name}:{func_path}"
370376
manifest["functions"][key] = _hash_function_bytecode(func)
371-
except (ImportError, Exception) as e:
377+
except (ImportError, OSError, TypeError, AttributeError) as e:
372378
logger.warning(
373379
"Could not hash function %s.%s: %s", mod_name, func_path, e
374380
)
375381

376-
with open(output_path, "w") as f:
382+
with open(output_path, "w", encoding="utf-8") as f:
377383
json.dump(manifest, f, indent=2)
378384

379385
return manifest

packages/agent-compliance/src/agent_compliance/verify.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -263,32 +263,43 @@ def verify(self) -> GovernanceAttestation:
263263

264264
def _check_control(self, control_id: str, spec: dict) -> ControlResult:
265265
"""Check if a single control's component is importable."""
266-
mod_name = spec["module"]
267-
component_name = spec["check"]
266+
mod_name = spec.get("module")
267+
component_name = spec.get("check")
268+
control_name = spec.get("name", control_id)
269+
270+
if not mod_name or not component_name:
271+
return ControlResult(
272+
control_id=control_id,
273+
name=control_name,
274+
present=False,
275+
module=mod_name or "",
276+
component=component_name or "",
277+
error="Malformed control spec: missing 'module' or 'check'",
278+
)
268279

269280
try:
270281
mod = importlib.import_module(mod_name)
271282
component = getattr(mod, component_name, None)
272283
if component is None:
273284
return ControlResult(
274285
control_id=control_id,
275-
name=spec["name"],
286+
name=control_name,
276287
present=False,
277288
module=mod_name,
278289
component=component_name,
279290
error=f"{component_name} not found in {mod_name}",
280291
)
281292
return ControlResult(
282293
control_id=control_id,
283-
name=spec["name"],
294+
name=control_name,
284295
present=True,
285296
module=mod_name,
286297
component=component_name,
287298
)
288299
except ImportError as e:
289300
return ControlResult(
290301
control_id=control_id,
291-
name=spec["name"],
302+
name=control_name,
292303
present=False,
293304
module=mod_name,
294305
component=component_name,

packages/agent-governance-dotnet/src/AgentGovernance/Policy/PolicyRule.cs

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -164,16 +164,43 @@ private static bool EvaluateExpression(string expression, IReadOnlyDictionary<st
164164
private static List<string> SplitCompound(string expression, string keyword)
165165
{
166166
var parts = new List<string>();
167-
int idx;
168-
var remaining = expression;
167+
var current = new System.Text.StringBuilder();
168+
bool inSingleQuote = false;
169+
bool inDoubleQuote = false;
169170

170-
while ((idx = remaining.IndexOf(keyword, StringComparison.OrdinalIgnoreCase)) >= 0)
171+
for (int i = 0; i < expression.Length; i++)
171172
{
172-
parts.Add(remaining[..idx]);
173-
remaining = remaining[(idx + keyword.Length)..];
173+
char c = expression[i];
174+
175+
// Toggle quote state (no escaping needed for policy expressions)
176+
if (c == '\'' && !inDoubleQuote)
177+
{
178+
inSingleQuote = !inSingleQuote;
179+
current.Append(c);
180+
continue;
181+
}
182+
if (c == '"' && !inSingleQuote)
183+
{
184+
inDoubleQuote = !inDoubleQuote;
185+
current.Append(c);
186+
continue;
187+
}
188+
189+
// Only split on keyword when outside quotes
190+
if (!inSingleQuote && !inDoubleQuote
191+
&& i + keyword.Length <= expression.Length
192+
&& expression.Substring(i, keyword.Length).Equals(keyword, StringComparison.OrdinalIgnoreCase))
193+
{
194+
parts.Add(current.ToString());
195+
current.Clear();
196+
i += keyword.Length - 1; // -1 because loop increments
197+
continue;
198+
}
199+
200+
current.Append(c);
174201
}
175202

176-
parts.Add(remaining);
203+
parts.Add(current.ToString());
177204
return parts;
178205
}
179206

packages/agent-governance-dotnet/src/AgentGovernance/Security/PromptInjectionDetector.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -251,11 +251,14 @@ public IReadOnlyList<DetectionResult> DetectBatch(IEnumerable<string> inputs)
251251
return inputs.Select(Detect).ToList().AsReadOnly();
252252
}
253253

254+
private static readonly int MaxCustomPatternLength = 1000;
255+
private static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(200);
256+
private static readonly Regex Base64Pattern = new(@"[A-Za-z0-9+/]{20,}={0,2}", RegexOptions.Compiled, TimeSpan.FromMilliseconds(200));
257+
254258
private (string Name, ThreatLevel Threat)? DetectEncodedPayloads(string input)
255259
{
256260
// Look for base64-encoded strings (at least 20 chars).
257-
var b64Pattern = new Regex(@"[A-Za-z0-9+/]{20,}={0,2}", RegexOptions.Compiled);
258-
var b64Matches = b64Pattern.Matches(input);
261+
var b64Matches = Base64Pattern.Matches(input);
259262

260263
foreach (Match match in b64Matches)
261264
{
@@ -309,9 +312,12 @@ public IReadOnlyList<DetectionResult> DetectBatch(IEnumerable<string> inputs)
309312
(Compile(@"UNION\s+SELECT", RegexOptions.IgnoreCase), InjectionType.DirectOverride, ThreatLevel.High, "sql_union"),
310313
};
311314

312-
// Add custom patterns.
315+
// Add custom patterns with length and timeout guards.
313316
foreach (var custom in _config.CustomPatterns)
314317
{
318+
if (custom.Length > MaxCustomPatternLength)
319+
continue;
320+
315321
try
316322
{
317323
patterns.Add((Compile(custom), InjectionType.DirectOverride, ThreatLevel.High, $"custom:{custom[..Math.Min(20, custom.Length)]}"));
@@ -327,7 +333,7 @@ public IReadOnlyList<DetectionResult> DetectBatch(IEnumerable<string> inputs)
327333

328334
private static Regex Compile(string pattern, RegexOptions extra = RegexOptions.None)
329335
{
330-
return new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase | extra);
336+
return new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase | extra, RegexTimeout);
331337
}
332338

333339
private static string ComputeHash(string input)

packages/agent-governance-dotnet/src/AgentGovernance/Sre/CircuitBreaker.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,8 @@ private void EnsureCallAllowed()
198198
switch (_state)
199199
{
200200
case CircuitState.Open:
201-
var elapsed = TimeSpan.FromMilliseconds(Environment.TickCount64 - _openedAtTicks);
201+
var elapsedMs = Math.Max(0, Environment.TickCount64 - _openedAtTicks);
202+
var elapsed = TimeSpan.FromMilliseconds(elapsedMs);
202203
var retryAfter = _config.ResetTimeout - elapsed;
203204
if (retryAfter < TimeSpan.Zero) retryAfter = TimeSpan.Zero;
204205
throw new CircuitBreakerOpenException(retryAfter);

packages/agent-hypervisor/src/hypervisor/integrations/iatp_adapter.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ def analyze_manifest(self, manifest: IATPManifest) -> ManifestAnalysis:
8787
trust_level = IATPTrustLevel.UNKNOWN
8888
ring_hint = TRUST_LEVEL_RING_HINTS.get(trust_level, ExecutionRing.RING_3_SANDBOX)
8989
iatp_score = manifest.calculate_trust_score()
90+
import math
91+
if not isinstance(iatp_score, (int, float)) or not math.isfinite(iatp_score):
92+
iatp_score = 0.0
93+
iatp_score = min(max(iatp_score, 0.0), 100.0)
9094
sigma_hint = min(max(iatp_score / 10.0, 0.0), 1.0)
9195
analysis = ManifestAnalysis(
9296
agent_did=agent_did, trust_level=trust_level, ring_hint=ring_hint,
@@ -107,6 +111,10 @@ def analyze_manifest_dict(self, manifest_dict: dict) -> ManifestAnalysis:
107111
trust_level = IATPTrustLevel.UNKNOWN
108112
ring_hint = TRUST_LEVEL_RING_HINTS.get(trust_level, ExecutionRing.RING_3_SANDBOX)
109113
iatp_score = manifest_dict.get("trust_score", 5)
114+
import math
115+
if not isinstance(iatp_score, (int, float)) or not math.isfinite(iatp_score):
116+
iatp_score = 0.0
117+
iatp_score = min(max(iatp_score, 0.0), 100.0)
110118
sigma_hint = min(max(iatp_score / 10.0, 0.0), 1.0)
111119
actions = []
112120
for cap in manifest_dict.get("actions", []):

packages/agent-hypervisor/src/hypervisor/models.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
from datetime import UTC, datetime
1010
from enum import Enum
1111

12-
# Agent ID must be alphanumeric, hyphens, underscores, colons, or dots (e.g. "did:mesh:agent-1")
13-
_AGENT_ID_PATTERN = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._:@-]*$")
12+
# Agent ID: DID format (did:method:id) or simple alphanumeric identifiers.
13+
# Restrict to safe characters — no @, no consecutive special chars.
14+
_AGENT_ID_PATTERN = re.compile(r"^[a-zA-Z0-9](?:[a-zA-Z0-9._:-]*[a-zA-Z0-9])?$")
1415
# Max lengths
1516
_MAX_AGENT_ID_LENGTH = 256
1617
_MAX_NAME_LENGTH = 256

packages/agent-hypervisor/src/hypervisor/providers.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,14 @@ def _discover_provider(group: str) -> type | None:
3636
if eps:
3737
ep = next(iter(eps))
3838
provider_cls = ep.load()
39-
_provider_cache[group] = provider_cls
40-
logger.info("Advanced provider loaded: %s from %s", ep.name, ep.value)
41-
return provider_cls
39+
if not isinstance(provider_cls, type):
40+
logger.warning(
41+
"Provider %s is not a class, skipping", ep.name
42+
)
43+
else:
44+
_provider_cache[group] = provider_cls
45+
logger.info("Advanced provider loaded: %s from %s", ep.name, ep.value)
46+
return provider_cls
4247
except Exception:
4348
logger.debug("Provider discovery failed for %s", group, exc_info=True)
4449

@@ -56,8 +61,8 @@ def get_ring_engine(**kwargs: Any):
5661
if provider is not None:
5762
return provider(**kwargs)
5863

59-
from hypervisor.rings.engine import RingEngine
60-
return RingEngine(**kwargs)
64+
from hypervisor.rings.enforcer import RingEnforcer
65+
return RingEnforcer(**kwargs)
6166

6267

6368
def get_liability_engine(**kwargs: Any):

0 commit comments

Comments
 (0)