mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-18 20:49:28 +00:00
feat(three-stage-scan): add complexity and regression config caps
This commit is contained in:
@@ -12,7 +12,7 @@ This plan ships the three-stage scan without rebuilding CWE policy or the regex
|
|||||||
- _Requirements: 1.1, 1.2, 1.3, 1.6_
|
- _Requirements: 1.1, 1.2, 1.3, 1.6_
|
||||||
- _Boundary: StagePlan_
|
- _Boundary: StagePlan_
|
||||||
|
|
||||||
- [ ] 1.2 Add complexity and regression configuration with candidate caps
|
- [x] 1.2 Add complexity and regression configuration with candidate caps
|
||||||
- Configuration exposes map top-k, max hunter hotspots, max regression candidates, and optional language image pins, with defaults that keep pull-request scans cheap.
|
- Configuration exposes map top-k, max hunter hotspots, max regression candidates, and optional language image pins, with defaults that keep pull-request scans cheap.
|
||||||
- Sandbox memory, pids, timeout, network-off, and read-only workspace remain the existing sandbox settings.
|
- Sandbox memory, pids, timeout, network-off, and read-only workspace remain the existing sandbox settings.
|
||||||
- Observable completion: loading config without a file yields those defaults; an override file changes top-k and max candidates only.
|
- Observable completion: loading config without a file yields those defaults; an override file changes top-k and max candidates only.
|
||||||
|
|||||||
@@ -96,6 +96,20 @@ class FusionConfig:
|
|||||||
high_assurance: bool = False
|
high_assurance: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ComplexityConfig:
|
||||||
|
# Stage-2 map size and hunter attention. Small defaults keep PR/nightly map work cheap.
|
||||||
|
top_k: int = 20
|
||||||
|
max_hunter_hotspots: int = 8
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class RegressConfig:
|
||||||
|
# Stage-3 candidate cap and optional per-language sandbox image pins. Isolation limits stay on SandboxConfig.
|
||||||
|
max_candidates: int = 5
|
||||||
|
images: tuple[tuple[str, str], ...] = ()
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ResolvedConfig:
|
class ResolvedConfig:
|
||||||
models: ModelConfig = ModelConfig()
|
models: ModelConfig = ModelConfig()
|
||||||
@@ -109,6 +123,8 @@ class ResolvedConfig:
|
|||||||
harnessx: HarnessxConfig = HarnessxConfig()
|
harnessx: HarnessxConfig = HarnessxConfig()
|
||||||
fusion: FusionConfig = FusionConfig()
|
fusion: FusionConfig = FusionConfig()
|
||||||
hardening: HardeningConfig = HardeningConfig()
|
hardening: HardeningConfig = HardeningConfig()
|
||||||
|
complexity: ComplexityConfig = ComplexityConfig()
|
||||||
|
regress: RegressConfig = RegressConfig()
|
||||||
runs_dir: str = ".openultrasast/runs"
|
runs_dir: str = ".openultrasast/runs"
|
||||||
|
|
||||||
|
|
||||||
@@ -130,6 +146,8 @@ def load_config(config_path: Path | None = None) -> ResolvedConfig:
|
|||||||
harnessx=_load_harnessx(data.get("harnessx", {})),
|
harnessx=_load_harnessx(data.get("harnessx", {})),
|
||||||
fusion=_load_fusion(data.get("fusion", {})),
|
fusion=_load_fusion(data.get("fusion", {})),
|
||||||
hardening=_load_hardening(data.get("hardening", {})),
|
hardening=_load_hardening(data.get("hardening", {})),
|
||||||
|
complexity=_load_complexity(data.get("complexity", {})),
|
||||||
|
regress=_load_regress(data.get("regress", {})),
|
||||||
runs_dir=os.environ.get("OPENULTRASAST_RUNS_DIR", ".openultrasast/runs"),
|
runs_dir=os.environ.get("OPENULTRASAST_RUNS_DIR", ".openultrasast/runs"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -251,6 +269,32 @@ def _load_fusion(value: object) -> FusionConfig:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_complexity(value: object) -> ComplexityConfig:
|
||||||
|
data = _section(value)
|
||||||
|
return ComplexityConfig(
|
||||||
|
top_k=_int_value(data.get("top_k"), 20),
|
||||||
|
max_hunter_hotspots=_int_value(data.get("max_hunter_hotspots"), 8),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_regress(value: object) -> RegressConfig:
|
||||||
|
data = _section(value)
|
||||||
|
return RegressConfig(
|
||||||
|
max_candidates=_int_value(data.get("max_candidates"), 5),
|
||||||
|
images=_string_map(data.get("images")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _string_map(value: object) -> tuple[tuple[str, str], ...]:
|
||||||
|
data = _section(value)
|
||||||
|
images: list[tuple[str, str]] = []
|
||||||
|
for key, item in data.items():
|
||||||
|
pinned = _string(item)
|
||||||
|
if pinned is not None:
|
||||||
|
images.append((str(key), pinned))
|
||||||
|
return tuple(sorted(images))
|
||||||
|
|
||||||
|
|
||||||
def _float_value(value: object, default: float) -> float:
|
def _float_value(value: object, default: float) -> float:
|
||||||
if isinstance(value, bool):
|
if isinstance(value, bool):
|
||||||
return default
|
return default
|
||||||
|
|||||||
@@ -96,3 +96,66 @@ def test_write_resolved_config_creates_json(tmp_path: Path) -> None:
|
|||||||
|
|
||||||
assert output.exists()
|
assert output.exists()
|
||||||
assert '"minimum_report_verified": "static_corroboration"' in output.read_text()
|
assert '"minimum_report_verified": "static_corroboration"' in output.read_text()
|
||||||
|
|
||||||
|
|
||||||
|
def test_complexity_and_regress_defaults_when_section_absent() -> None:
|
||||||
|
config = load_config(None)
|
||||||
|
|
||||||
|
assert config.complexity.top_k == 20
|
||||||
|
assert config.complexity.max_hunter_hotspots == 8
|
||||||
|
assert config.regress.max_candidates == 5
|
||||||
|
assert config.regress.images == ()
|
||||||
|
assert config.sandbox.network is False
|
||||||
|
assert config.sandbox.workspace_readonly is True
|
||||||
|
assert config.sandbox.memory_mb == 2048
|
||||||
|
assert config.sandbox.timeout_seconds == 300
|
||||||
|
assert config.sandbox.pids_limit == 512
|
||||||
|
|
||||||
|
|
||||||
|
def test_override_changes_top_k_and_max_candidates_only(tmp_path: Path) -> None:
|
||||||
|
config_path = tmp_path / "openultrasast.toml"
|
||||||
|
config_path.write_text(
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
"[complexity]",
|
||||||
|
"top_k = 3",
|
||||||
|
"[regress]",
|
||||||
|
"max_candidates = 2",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
config = load_config(config_path)
|
||||||
|
|
||||||
|
assert config.complexity.top_k == 3
|
||||||
|
assert config.regress.max_candidates == 2
|
||||||
|
assert config.complexity.max_hunter_hotspots == 8
|
||||||
|
assert config.regress.images == ()
|
||||||
|
assert config.sandbox.network is False
|
||||||
|
assert config.sandbox.workspace_readonly is True
|
||||||
|
assert config.sandbox.memory_mb == 2048
|
||||||
|
assert config.sandbox.timeout_seconds == 300
|
||||||
|
assert config.sandbox.pids_limit == 512
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_reads_regress_image_pins(tmp_path: Path) -> None:
|
||||||
|
config_path = tmp_path / "openultrasast.toml"
|
||||||
|
config_path.write_text(
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
"[regress]",
|
||||||
|
"max_candidates = 4",
|
||||||
|
"[regress.images]",
|
||||||
|
'python = "python:3.12-slim"',
|
||||||
|
'javascript = "node:22-slim"',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
config = load_config(config_path)
|
||||||
|
|
||||||
|
assert config.regress.max_candidates == 4
|
||||||
|
assert dict(config.regress.images) == {"python": "python:3.12-slim", "javascript": "node:22-slim"}
|
||||||
|
assert config.complexity.top_k == 20
|
||||||
|
assert config.complexity.max_hunter_hotspots == 8
|
||||||
|
assert config.sandbox.memory_mb == 2048
|
||||||
|
|||||||
Reference in New Issue
Block a user