Skip to content
1 change: 1 addition & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,4 @@
**Vulnerability:** Denial of Service / Availability
**Learning:** Strix security scanners crashed when the backend LLM returned an 'HTTP Error 502: Bad Gateway' response. This was because 'bad gateway' string match and generic 'APIError' were missing from the `is_llm_api_connection_error` function in the Strix retry gate.
**Prevention:** Always include `bad gateway` and `APIError` in string match conditions when handling HTTP API Connection exceptions for LLM backends to ensure proper fail-closed and retry handling.
## 2026-09-29 - Prevent Semgrep SSRF false positive\n**Vulnerability:** Server-Side Request Forgery (SSRF) / False Positive\n**Learning:** Semgrep flags urllib.request.urlopen as a potential SSRF or LFI vulnerability if it reads from a dynamic URL. In our maturin asset fetcher, the URL is strictly prefixed by a hardcoded GitHub releases path, mitigating this risk, but the scanner still warns. \n**Prevention:** Added `nosec` and `nosemgrep` comments specifically to the `urlopen` call after validating that the URL construction is secure and cannot be manipulated by untrusted user input.
2 changes: 1 addition & 1 deletion scripts/ci/strix_report_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def validate(output: Path, changed_paths: list[str]) -> None:
raise ValueError("scan report does not identify a changed source file")


if __name__ == "__main__":
if __name__ == "__main__": # pragma: no cover
try:
validate(Path(sys.argv[1]), sys.argv[2:])
except (IndexError, OSError, ValueError, TypeError) as error:
Expand Down
1 change: 1 addition & 0 deletions scripts/ci/verify_release_distribution_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ def fetch_artifact(repository: str, artifact_id: int, output: BinaryIO) -> None:
process = subprocess.Popen(
["gh", "api", f"repos/{repository}/actions/artifacts/{artifact_id}/zip"],
stdout=subprocess.PIPE,
shell=False,
)
try:
while block := process.stdout.read(1024 * 1024):
Expand Down
5 changes: 4 additions & 1 deletion scripts/ci/verify_release_maturin_tool_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@

def _download(filename: str) -> bytes:
url = f"https://github.com/PyO3/maturin/releases/download/v1.15.0/{filename}"
with urlopen(Request(url, headers={"User-Agent": "cwl-release-gate"}), timeout=60) as response:
# Fixed https origin and tag; verify_assets admits only five literal asset names.
with urlopen( # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected # nosec B310
Request(url, headers={"User-Agent": "cwl-release-gate"}), timeout=60
) as response:
raw = response.read(MAX_ASSET_BYTES + 1)
if len(raw) > MAX_ASSET_BYTES:
raise ValueError("maturin release asset exceeds inspection limit")
Expand Down
51 changes: 51 additions & 0 deletions tests/test_strix_report_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,54 @@ def test_report_scope_rejects_unrelated_success_and_accepts_scoped_success(tmp_p
report.write_text("Assessed python/fast_mlsirm/report.py; no vulnerabilities found.\n", encoding="utf-8")
assert subprocess.run(command, capture_output=True).returncode == 0
assert subprocess.run(command[:-1], capture_output=True).returncode == 1

def test_validate_exceptions(tmp_path: Path) -> None:
import scripts.ci.strix_report_scope as strix_report_scope
import pytest

with pytest.raises(ValueError, match="scan output directory is missing"):
strix_report_scope.validate(tmp_path / "nonexistent", [])

run = tmp_path / "current-scan"
run.mkdir()

with pytest.raises(ValueError, match="scan report files are missing or linked"):
strix_report_scope.validate(tmp_path, [])

(run / "run.json").write_text("[]", encoding="utf-8")
(run / "penetration_test_report.md").write_text("", encoding="utf-8")

with pytest.raises(ValueError, match="scan metadata is not an object"):
strix_report_scope.validate(tmp_path, [])

(run / "run.json").write_text(json.dumps({"status": "completed", "scan_results": "[]"}), encoding="utf-8")
with pytest.raises(ValueError, match="scan results are not an object"):
strix_report_scope.validate(tmp_path, [])

(run / "run.json").write_text(json.dumps({"scan_results": {}}), encoding="utf-8")
with pytest.raises(ValueError, match="scan report is incomplete"):
strix_report_scope.validate(tmp_path, [])

(run / "run.json").write_text(json.dumps({"status": "completed"}), encoding="utf-8")
with pytest.raises(ValueError, match="scan report is incomplete"):
strix_report_scope.validate(tmp_path, [])

(run / "run.json").write_text(json.dumps({"status": "completed", "scan_results": {"scan_completed": True, "success": False}}), encoding="utf-8")
with pytest.raises(ValueError, match="scan report is incomplete"):
strix_report_scope.validate(tmp_path, [])

(run / "run.json").write_text(json.dumps({"status": "completed", "scan_results": {"scan_completed": False, "success": True}}), encoding="utf-8")
with pytest.raises(ValueError, match="scan report is incomplete"):
strix_report_scope.validate(tmp_path, ["python/fast_mlsirm/report.py"])

(run / "run.json").write_text(json.dumps({"status": "completed", "scan_results": {"scan_completed": True, "success": True}}), encoding="utf-8")
with pytest.raises(ValueError, match="scan report does not identify a changed source file"):
strix_report_scope.validate(tmp_path, ["python/fast_mlsirm/report.py"])

(run / "penetration_test_report.md").write_text("python/fast_mlsirm/report.py", encoding="utf-8")
strix_report_scope.validate(tmp_path, ["python/fast_mlsirm/report.py"])

run2 = tmp_path / "another-scan"
run2.mkdir(parents=True)
with pytest.raises(ValueError, match="expected exactly one current scan report"):
strix_report_scope.validate(tmp_path, [])
4 changes: 2 additions & 2 deletions tests/test_verify_release_distribution_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ def kill(self):

processes: list[Process] = []

def popen(_args, stdout):
def popen(_args, stdout, shell=False):
assert stdout is subprocess.PIPE
process = processes.pop(0)
return process
Expand Down Expand Up @@ -539,7 +539,7 @@ def wait() -> int:
def poll() -> int:
return 0

def popen(args, stdout):
def popen(args, stdout, shell=False):
assert stdout is subprocess.PIPE
artifact_id = int(args[2].split("/")[-2])
return Process(case["archives"][artifact_id])
Expand Down
2 changes: 1 addition & 1 deletion tests/test_verify_release_scope_evidence_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -1458,7 +1458,7 @@ def wait() -> int:
def poll() -> int:
return 0

def popen(args, stdout):
def popen(args, stdout, shell=False):
assert stdout is subprocess.PIPE
artifact_id = int(args[2].split("/")[-2])
return Process(case["archives"][artifact_id])
Expand Down
Loading