mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-17 20:19:29 +00:00
records whether its decision beat noise Two things the maintainer was right about. Thinking was disabled so `temperature: 0` would not be silently ignored -- a trade that only pays if it buys determinism. Round zero says it did not: 11 of 20 injection pairs disagreed with themselves across five runs, so the variance is in the agentic path, not the decoder. We paid the reasoning cost on a reasoning model and got nothing. It is now `[learning] thinking`, recorded in the artifact and part of its key so the two modes cannot be read as one number. The default stays off only because that is what round zero was measured under. And the deeper point: the spec built `sign_test` and `reliable_change` and the acceptance rule calls neither -- `decide` compares raw deltas. On a detector with that floor a raw delta is mostly noise, and a rule that brittle is exactly what makes a deterministic decoder look necessary. It is not; the alternative is an acceptance rule that models the score as noisy, the way reflective-evolution and Bayesian prompt optimizers do. Changing Req 9.5 is a maintainer's call, so this does not change the decision: every round now journals the two-sided sign test over the pairs that moved, and after four rounds the journal will say whether any decision could be told from a coin flip. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
411 lines
14 KiB
Python
411 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import tomllib
|
|
from dataclasses import asdict, dataclass
|
|
from pathlib import Path
|
|
|
|
DEFAULT_VECTOR_STORE = "json-local"
|
|
|
|
|
|
def load_dotenv(path: Path | None = None, *, force: bool = False) -> None:
|
|
"""Load KEY=VALUE lines from .env into os.environ. Stdlib only; never overrides."""
|
|
if not force and (os.environ.get("PYTEST_CURRENT_TEST") or os.environ.get("OPENULTRASAST_SKIP_DOTENV")):
|
|
return
|
|
env_path = path if path is not None else Path(".env")
|
|
if not env_path.is_file():
|
|
return
|
|
for raw in env_path.read_text().splitlines():
|
|
line = raw.strip()
|
|
if not line or line.startswith("#") or "=" not in line:
|
|
continue
|
|
key, _, value = line.partition("=")
|
|
key = key.strip()
|
|
value = value.strip().strip("'").strip('"')
|
|
if key and key not in os.environ:
|
|
os.environ[key] = value
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ModelConfig:
|
|
ranker: str | None = None
|
|
hunter: str | None = None
|
|
verifier: str | None = None
|
|
patcher: str | None = None
|
|
judge: str | None = None # second, independent judgement before anything is published as proven
|
|
chat_base_url: str | None = None # chat endpoint; independent of the embedding endpoint
|
|
chat_api_key_env: str | None = None # environment variable holding that endpoint's key
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class EmbeddingConfig:
|
|
model: str | None = None
|
|
store: str = DEFAULT_VECTOR_STORE
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SandboxConfig:
|
|
network: bool = False
|
|
workspace_readonly: bool = True
|
|
memory_mb: int = 2048
|
|
timeout_seconds: int = 300
|
|
pids_limit: int = 512
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DynamicConfig:
|
|
enabled: bool = False
|
|
network_scope: tuple[str, ...] = ()
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class EvidenceConfig:
|
|
minimum_report_verified: str = "static_corroboration"
|
|
minimum_exploit: str = "crash_reproduced"
|
|
minimum_patch: str = "root_cause_explained"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class StaticAnalysisConfig:
|
|
sarif_paths: tuple[str, ...] = ()
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ScoreConfig:
|
|
k: float = 60.0
|
|
min_score: int = 80
|
|
block_severity_reachable: int = 5
|
|
blocking: bool = False
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RulesetConfig:
|
|
# Emission floors that replace "every regex match becomes a finding". The
|
|
# defaults (0.0) emit everything, keeping output byte-identical by default.
|
|
min_emit_priority: float = 0.0
|
|
min_emit_precision: float = 0.0
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class HarnessxConfig:
|
|
# Optional agentic plane (the openultrasast[harnessx] extra). `provider`
|
|
# selects the HarnessX LLM provider used by both the hunter pool and the
|
|
# llm-judge verifier; the budget caps bound per-task cost and token spend.
|
|
# Inert unless the extra is installed and a model is configured in [models].
|
|
provider: str = "anthropic"
|
|
max_cost_usd: float = 2.0
|
|
token_threshold: int = 120_000
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class HardeningConfig:
|
|
# Release-readiness controls. redact_secrets masks credentials in traces/reports;
|
|
# max_findings caps the reported finding count for bounded CI runs (0 = unlimited).
|
|
redact_secrets: bool = True
|
|
max_findings: int = 0
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class FusionConfig:
|
|
# Two-panel fusion adjudication for triggered findings (standard mode). Runs
|
|
# deterministically by default; setting panel_model routes the panels through
|
|
# the configured HarnessX provider. high_assurance forces fusion on every finding.
|
|
enabled: bool = True
|
|
panel_model: str | None = None
|
|
decider_model: str | None = None
|
|
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)
|
|
class VariantsConfig:
|
|
# corpus-seeded-mechanisms: structural variant search in MAP (standard/deep only).
|
|
enabled: bool = True
|
|
max_mechanisms: int = 500
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LearningConfig:
|
|
# learning-harness: the classified detector set and its rounds.
|
|
enabled: bool = False # off until round zero has written the family configurations this reads
|
|
families_path: str | None = None # taxonomy file; the shipped ruleset when unset
|
|
configs_dir: str = ".openultrasast/learning/configs"
|
|
k_runs: int = 5 # runs per pair; a single run misses most real changes
|
|
round_cost_cap_usd: float = 10.0
|
|
minibatch: int = 8 # train pairs in a round's first stage
|
|
detector_model: str | None = None
|
|
judge_model: str | None = None
|
|
cutoff_date: str | None = None # rows fixed after this date are reported as a separate slice
|
|
# Whether the detector model reasons before answering. Off by default because that is what round zero was
|
|
# measured under, not because it is known to be better: the provider silently ignores `temperature` while
|
|
# thinking, and thinking was disabled to make `temperature: 0` mean something. It did not — the measured
|
|
# disagreement between runs comes from the agentic path, not the decoder — so which mode wins is a question
|
|
# for a second baseline rather than for a default (Req 8.3).
|
|
thinking: bool = False
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ObligationsConfig:
|
|
# authorization-obligations: the obligation checker in MAP (standard/deep only).
|
|
enabled: bool = True
|
|
min_siblings: int = 3
|
|
policy_path: str = ".openultrasast/obligations.toml"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ResolvedConfig:
|
|
models: ModelConfig = ModelConfig()
|
|
embeddings: EmbeddingConfig = EmbeddingConfig()
|
|
sandbox: SandboxConfig = SandboxConfig()
|
|
dynamic: DynamicConfig = DynamicConfig()
|
|
evidence: EvidenceConfig = EvidenceConfig()
|
|
static_analysis: StaticAnalysisConfig = StaticAnalysisConfig()
|
|
score: ScoreConfig = ScoreConfig()
|
|
ruleset: RulesetConfig = RulesetConfig()
|
|
harnessx: HarnessxConfig = HarnessxConfig()
|
|
fusion: FusionConfig = FusionConfig()
|
|
hardening: HardeningConfig = HardeningConfig()
|
|
complexity: ComplexityConfig = ComplexityConfig()
|
|
regress: RegressConfig = RegressConfig()
|
|
variants: VariantsConfig = VariantsConfig()
|
|
obligations: ObligationsConfig = ObligationsConfig()
|
|
learning: LearningConfig = LearningConfig()
|
|
runs_dir: str = ".openultrasast/runs"
|
|
|
|
|
|
def load_config(config_path: Path | None = None) -> ResolvedConfig:
|
|
load_dotenv()
|
|
data: dict[str, object] = {}
|
|
if config_path is not None and config_path.exists():
|
|
with config_path.open("rb") as handle:
|
|
data = tomllib.load(handle)
|
|
|
|
return ResolvedConfig(
|
|
models=_load_models(data.get("models", {})),
|
|
embeddings=_load_embeddings(data.get("embeddings", {})),
|
|
sandbox=_load_sandbox(data.get("sandbox", {})),
|
|
dynamic=_load_dynamic(data.get("dynamic", {})),
|
|
evidence=_load_evidence(data.get("evidence", {})),
|
|
static_analysis=_load_static_analysis(data.get("static_analysis", {})),
|
|
score=_load_score(data.get("score", {})),
|
|
ruleset=_load_ruleset_config(data.get("ruleset", {})),
|
|
harnessx=_load_harnessx(data.get("harnessx", {})),
|
|
fusion=_load_fusion(data.get("fusion", {})),
|
|
hardening=_load_hardening(data.get("hardening", {})),
|
|
complexity=_load_complexity(data.get("complexity", {})),
|
|
regress=_load_regress(data.get("regress", {})),
|
|
variants=_load_variants(data.get("variants", {})),
|
|
obligations=_load_obligations(data.get("obligations", {})),
|
|
learning=_load_learning(data.get("learning", {})),
|
|
runs_dir=os.environ.get("OPENULTRASAST_RUNS_DIR", ".openultrasast/runs"),
|
|
)
|
|
|
|
|
|
def write_resolved_config(config: ResolvedConfig, path: Path) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(asdict(config), indent=2, sort_keys=True) + "\n")
|
|
|
|
|
|
def _section(value: object) -> dict[str, object]:
|
|
return value if isinstance(value, dict) else {}
|
|
|
|
|
|
def _string(value: object) -> str | None:
|
|
return value if isinstance(value, str) and value else None
|
|
|
|
|
|
def _load_models(value: object) -> ModelConfig:
|
|
data = _section(value)
|
|
return ModelConfig(
|
|
ranker=_string(data.get("ranker")),
|
|
hunter=_string(data.get("hunter")),
|
|
verifier=_string(data.get("verifier")),
|
|
patcher=_string(data.get("patcher")),
|
|
judge=_string(data.get("judge")),
|
|
chat_base_url=_string(data.get("chat_base_url")),
|
|
chat_api_key_env=_string(data.get("chat_api_key_env")),
|
|
)
|
|
|
|
|
|
def _load_embeddings(value: object) -> EmbeddingConfig:
|
|
data = _section(value)
|
|
return EmbeddingConfig(
|
|
model=_string(data.get("model")) or _string(os.environ.get("OPENROUTER_EMBEDDING_MODEL")),
|
|
store=_string(data.get("store")) or DEFAULT_VECTOR_STORE,
|
|
)
|
|
|
|
|
|
def _load_sandbox(value: object) -> SandboxConfig:
|
|
data = _section(value)
|
|
return SandboxConfig(
|
|
network=bool(data.get("network", False)),
|
|
workspace_readonly=bool(data.get("workspace_readonly", True)),
|
|
memory_mb=_int_value(data.get("memory_mb"), 2048),
|
|
timeout_seconds=_int_value(data.get("timeout_seconds"), 300),
|
|
pids_limit=_int_value(data.get("pids_limit"), 512),
|
|
)
|
|
|
|
|
|
def _load_dynamic(value: object) -> DynamicConfig:
|
|
data = _section(value)
|
|
scope = data.get("network_scope", [])
|
|
if not isinstance(scope, list):
|
|
scope = []
|
|
return DynamicConfig(enabled=bool(data.get("enabled", False)), network_scope=tuple(str(item) for item in scope))
|
|
|
|
|
|
def _load_evidence(value: object) -> EvidenceConfig:
|
|
data = _section(value)
|
|
return EvidenceConfig(
|
|
minimum_report_verified=str(data.get("minimum_report_verified", "static_corroboration")),
|
|
minimum_exploit=str(data.get("minimum_exploit", "crash_reproduced")),
|
|
minimum_patch=str(data.get("minimum_patch", "root_cause_explained")),
|
|
)
|
|
|
|
|
|
def _int_value(value: object, default: int) -> int:
|
|
if isinstance(value, bool):
|
|
return default
|
|
if isinstance(value, int | str | bytes | bytearray):
|
|
return int(value)
|
|
return default
|
|
|
|
|
|
def _load_static_analysis(value: object) -> StaticAnalysisConfig:
|
|
data = _section(value)
|
|
paths = data.get("sarif_paths", [])
|
|
if not isinstance(paths, list):
|
|
paths = []
|
|
return StaticAnalysisConfig(sarif_paths=tuple(str(item) for item in paths))
|
|
|
|
|
|
def _load_score(value: object) -> ScoreConfig:
|
|
data = _section(value)
|
|
return ScoreConfig(
|
|
k=_float_value(data.get("k"), 60.0),
|
|
min_score=_int_value(data.get("min_score"), 80),
|
|
block_severity_reachable=_int_value(data.get("block_severity_reachable"), 5),
|
|
blocking=bool(data.get("blocking", False)),
|
|
)
|
|
|
|
|
|
def _load_ruleset_config(value: object) -> RulesetConfig:
|
|
data = _section(value)
|
|
return RulesetConfig(
|
|
min_emit_priority=_float_value(data.get("min_emit_priority"), 0.0),
|
|
min_emit_precision=_float_value(data.get("min_emit_precision"), 0.0),
|
|
)
|
|
|
|
|
|
def _load_harnessx(value: object) -> HarnessxConfig:
|
|
data = _section(value)
|
|
return HarnessxConfig(
|
|
provider=_string(data.get("provider")) or "anthropic",
|
|
max_cost_usd=_float_value(data.get("max_cost_usd"), 2.0),
|
|
token_threshold=_int_value(data.get("token_threshold"), 120_000),
|
|
)
|
|
|
|
|
|
def _load_hardening(value: object) -> HardeningConfig:
|
|
data = _section(value)
|
|
return HardeningConfig(
|
|
redact_secrets=bool(data.get("redact_secrets", True)),
|
|
max_findings=_int_value(data.get("max_findings"), 0),
|
|
)
|
|
|
|
|
|
def _load_fusion(value: object) -> FusionConfig:
|
|
data = _section(value)
|
|
return FusionConfig(
|
|
enabled=bool(data.get("enabled", True)),
|
|
panel_model=_string(data.get("panel_model")),
|
|
decider_model=_string(data.get("decider_model")),
|
|
high_assurance=bool(data.get("high_assurance", False)),
|
|
)
|
|
|
|
|
|
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:
|
|
if isinstance(value, bool):
|
|
return default
|
|
if isinstance(value, int | float):
|
|
return float(value)
|
|
if isinstance(value, str):
|
|
try:
|
|
return float(value)
|
|
except ValueError:
|
|
return default
|
|
return default
|
|
|
|
|
|
def _load_variants(value: object) -> VariantsConfig:
|
|
data = _section(value)
|
|
return VariantsConfig(
|
|
enabled=bool(data.get("enabled", True)),
|
|
max_mechanisms=_int_value(data.get("max_mechanisms"), 500),
|
|
)
|
|
|
|
|
|
def _load_learning(value: object) -> LearningConfig:
|
|
data = _section(value)
|
|
return LearningConfig(
|
|
enabled=bool(data.get("enabled", LearningConfig.enabled)),
|
|
families_path=_string(data.get("families_path")),
|
|
configs_dir=_string(data.get("configs_dir")) or LearningConfig.configs_dir,
|
|
k_runs=_int_value(data.get("k_runs"), LearningConfig.k_runs),
|
|
round_cost_cap_usd=_float_value(data.get("round_cost_cap_usd"), LearningConfig.round_cost_cap_usd),
|
|
minibatch=_int_value(data.get("minibatch"), LearningConfig.minibatch),
|
|
detector_model=_string(data.get("detector_model")),
|
|
judge_model=_string(data.get("judge_model")),
|
|
cutoff_date=_string(data.get("cutoff_date")),
|
|
thinking=bool(data.get("thinking", False)),
|
|
)
|
|
|
|
|
|
def _load_obligations(value: object) -> ObligationsConfig:
|
|
data = _section(value)
|
|
return ObligationsConfig(
|
|
enabled=bool(data.get("enabled", True)),
|
|
min_siblings=_int_value(data.get("min_siblings"), 3),
|
|
policy_path=str(data.get("policy_path", ".openultrasast/obligations.toml")),
|
|
)
|