CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
267 lines
10 KiB
Python
267 lines
10 KiB
Python
# Copyright Advanced Micro Devices, Inc.
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
sys.path.insert(0, os.fspath(Path(__file__).parent.parent))
|
|
from gitleaks import (
|
|
_CONFIG_PATH,
|
|
_LEAK_SECURITY_SEVERITY_HIGH,
|
|
_determine_log_opts,
|
|
_enrich_sarif_with_security_severity,
|
|
_md_code_fence,
|
|
_parse_report_formats,
|
|
_resolve_config_path,
|
|
)
|
|
|
|
|
|
class ParseReportFormatsTest(unittest.TestCase):
|
|
"""Tests for `_parse_report_formats`."""
|
|
|
|
def test_default_sarif(self):
|
|
targets = _parse_report_formats("sarif")
|
|
self.assertEqual(len(targets), 1)
|
|
self.assertEqual(targets[0].fmt, "sarif")
|
|
self.assertEqual(targets[0].path, Path("gitleaks-report.sarif"))
|
|
|
|
def test_multiple_formats_with_whitespace_and_dedup(self):
|
|
targets = _parse_report_formats(" sarif , json , sarif ,csv ")
|
|
self.assertEqual([t.fmt for t in targets], ["sarif", "json", "csv"])
|
|
self.assertEqual(
|
|
[t.path for t in targets],
|
|
[
|
|
Path("gitleaks-report.sarif"),
|
|
Path("gitleaks-report.json"),
|
|
Path("gitleaks-report.csv"),
|
|
],
|
|
)
|
|
|
|
def test_junit_uses_xml_extension(self):
|
|
targets = _parse_report_formats("junit")
|
|
self.assertEqual(targets[0].path, Path("gitleaks-report.xml"))
|
|
|
|
def test_empty_input_raises(self):
|
|
with self.assertRaises(ValueError) as ctx:
|
|
_parse_report_formats("")
|
|
self.assertIn("report_formats is empty", str(ctx.exception))
|
|
|
|
def test_only_whitespace_raises(self):
|
|
with self.assertRaises(ValueError):
|
|
_parse_report_formats(" , , ")
|
|
|
|
def test_unknown_format_raises(self):
|
|
with self.assertRaises(ValueError) as ctx:
|
|
_parse_report_formats("sarif,xml")
|
|
self.assertIn("'xml'", str(ctx.exception))
|
|
|
|
|
|
class DetermineLogOptsTest(unittest.TestCase):
|
|
"""Tests for `_determine_log_opts`."""
|
|
|
|
def test_scan_mode_all_returns_empty(self):
|
|
self.assertEqual(_determine_log_opts("all", "pull_request", {}), "")
|
|
self.assertEqual(_determine_log_opts("all", "release", {"unrelated": 1}), "")
|
|
|
|
def test_pull_request_returns_sha_range_without_no_merges(self):
|
|
event = {"pull_request": {"base": {"sha": "aaa"}, "head": {"sha": "bbb"}}}
|
|
with mock.patch("gitleaks.subprocess.run") as run:
|
|
# `_determine_log_opts` does a best-effort fetch and then
|
|
# verifies the base commit is reachable with `rev-parse`.
|
|
run.side_effect = [
|
|
mock.Mock(returncode=0, stderr=""),
|
|
mock.Mock(returncode=0, stderr=""),
|
|
]
|
|
log_opts = _determine_log_opts("changed", "pull_request", event)
|
|
self.assertEqual(log_opts, "aaa..bbb")
|
|
self.assertNotIn("--no-merges", log_opts)
|
|
|
|
def test_pull_request_target_is_explicitly_rejected(self):
|
|
event = {"pull_request": {"base": {"sha": "aaa"}, "head": {"sha": "bbb"}}}
|
|
with self.assertRaises(ValueError) as ctx:
|
|
_determine_log_opts("changed", "pull_request_target", event)
|
|
self.assertIn("pull_request_target is not supported", str(ctx.exception))
|
|
|
|
def test_push_returns_sha_range_without_no_merges(self):
|
|
log_opts = _determine_log_opts(
|
|
"changed", "push", {"before": "xxx", "after": "yyy"}
|
|
)
|
|
self.assertEqual(log_opts, "xxx..yyy")
|
|
self.assertNotIn("--no-merges", log_opts)
|
|
|
|
def test_push_new_ref_returns_empty(self):
|
|
log_opts = _determine_log_opts(
|
|
"changed", "push", {"before": "0" * 40, "after": "yyy"}
|
|
)
|
|
self.assertEqual(log_opts, "")
|
|
|
|
def test_unknown_event_type_raises(self):
|
|
with self.assertRaises(ValueError) as ctx:
|
|
_determine_log_opts("changed", "release", {})
|
|
self.assertIn("'release'", str(ctx.exception))
|
|
self.assertIn("scan_mode='all'", str(ctx.exception))
|
|
|
|
def test_unset_event_name_raises(self):
|
|
with self.assertRaises(ValueError) as ctx:
|
|
_determine_log_opts("changed", "", {})
|
|
self.assertIn("'<unset>'", str(ctx.exception))
|
|
|
|
def test_pull_request_malformed_payload_raises_key_error(self):
|
|
with self.assertRaises(KeyError):
|
|
_determine_log_opts("changed", "pull_request", {"pull_request": {}})
|
|
|
|
def test_push_malformed_payload_raises_key_error(self):
|
|
with self.assertRaises(KeyError):
|
|
_determine_log_opts("changed", "push", {})
|
|
|
|
|
|
class EnrichSarifTest(unittest.TestCase):
|
|
"""Tests for `_enrich_sarif_with_security_severity`."""
|
|
|
|
def _write_sarif(self, payload: object) -> Path:
|
|
fd, name = tempfile.mkstemp(suffix=".sarif")
|
|
os.close(fd)
|
|
path = Path(name)
|
|
path.write_text(json.dumps(payload), encoding="utf-8")
|
|
self.addCleanup(path.unlink, missing_ok=True)
|
|
return path
|
|
|
|
def test_backfills_level_and_security_severity(self):
|
|
path = self._write_sarif(
|
|
{"runs": [{"results": [{"message": {"text": "leak"}}]}]}
|
|
)
|
|
_enrich_sarif_with_security_severity(path)
|
|
data = json.loads(path.read_text())
|
|
result = data["runs"][0]["results"][0]
|
|
self.assertEqual(result["level"], "error")
|
|
self.assertEqual(
|
|
result["properties"]["security-severity"],
|
|
_LEAK_SECURITY_SEVERITY_HIGH,
|
|
)
|
|
|
|
def test_preserves_existing_level(self):
|
|
path = self._write_sarif({"runs": [{"results": [{"level": "warning"}]}]})
|
|
_enrich_sarif_with_security_severity(path)
|
|
data = json.loads(path.read_text())
|
|
self.assertEqual(data["runs"][0]["results"][0]["level"], "warning")
|
|
|
|
def test_preserves_existing_security_severity(self):
|
|
path = self._write_sarif(
|
|
{"runs": [{"results": [{"properties": {"security-severity": "3.5"}}]}]}
|
|
)
|
|
_enrich_sarif_with_security_severity(path)
|
|
data = json.loads(path.read_text())
|
|
self.assertEqual(
|
|
data["runs"][0]["results"][0]["properties"]["security-severity"],
|
|
"3.5",
|
|
)
|
|
|
|
def test_empty_runs_raises(self):
|
|
path = self._write_sarif({"runs": []})
|
|
original = path.read_text()
|
|
with self.assertRaises(ValueError) as ctx:
|
|
_enrich_sarif_with_security_severity(path)
|
|
self.assertIn("empty 'runs' array", str(ctx.exception))
|
|
# File is left untouched when we bail out.
|
|
self.assertEqual(path.read_text(), original)
|
|
|
|
def test_clean_scan_with_empty_results_is_valid(self):
|
|
# Gitleaks emits {"runs": [{"results": [], ...}]} on a clean scan;
|
|
# that's a valid SARIF and must NOT raise (this is the normal,
|
|
# no-leaks-found path).
|
|
path = self._write_sarif({"runs": [{"results": []}]})
|
|
_enrich_sarif_with_security_severity(path)
|
|
data = json.loads(path.read_text())
|
|
self.assertEqual(data["runs"][0]["results"], [])
|
|
|
|
def test_malformed_top_level_raises(self):
|
|
path = self._write_sarif(["not", "a", "dict"])
|
|
original = path.read_text()
|
|
with self.assertRaises(ValueError) as ctx:
|
|
_enrich_sarif_with_security_severity(path)
|
|
self.assertIn("top-level must be a JSON object", str(ctx.exception))
|
|
# File should be left unchanged when payload is unexpectedly shaped.
|
|
self.assertEqual(path.read_text(), original)
|
|
|
|
def test_invalid_json_raises(self):
|
|
fd, name = tempfile.mkstemp(suffix=".sarif")
|
|
os.close(fd)
|
|
path = Path(name)
|
|
self.addCleanup(path.unlink, missing_ok=True)
|
|
path.write_text("{not valid json", encoding="utf-8")
|
|
with self.assertRaises(ValueError) as ctx:
|
|
_enrich_sarif_with_security_severity(path)
|
|
self.assertIn("not valid JSON", str(ctx.exception))
|
|
|
|
def test_runs_must_be_a_list(self):
|
|
path = self._write_sarif({"runs": "oops"})
|
|
with self.assertRaises(ValueError) as ctx:
|
|
_enrich_sarif_with_security_severity(path)
|
|
self.assertIn("'runs' must be a list", str(ctx.exception))
|
|
|
|
def test_missing_file_raises(self):
|
|
path = Path(tempfile.gettempdir()) / "does-not-exist.sarif"
|
|
if path.exists():
|
|
path.unlink()
|
|
with self.assertRaises(FileNotFoundError):
|
|
_enrich_sarif_with_security_severity(path)
|
|
|
|
|
|
class ResolveConfigPathTest(unittest.TestCase):
|
|
"""Tests for `_resolve_config_path`."""
|
|
|
|
def setUp(self):
|
|
# `_resolve_config_path` resolves _CONFIG_PATH relative to cwd, so
|
|
# each test runs in its own tempdir to isolate the lookup.
|
|
self._original_cwd = Path.cwd()
|
|
self._tmp = tempfile.TemporaryDirectory()
|
|
os.chdir(self._tmp.name)
|
|
|
|
def tearDown(self):
|
|
os.chdir(self._original_cwd)
|
|
self._tmp.cleanup()
|
|
|
|
def test_returns_config_path_when_present(self):
|
|
Path(_CONFIG_PATH).write_text("# stub config", encoding="utf-8")
|
|
self.assertEqual(_resolve_config_path(), _CONFIG_PATH)
|
|
|
|
def test_raises_when_missing(self):
|
|
with self.assertRaises(FileNotFoundError) as ctx:
|
|
_resolve_config_path()
|
|
self.assertIn(_CONFIG_PATH, str(ctx.exception))
|
|
|
|
|
|
class MdCodeFenceTest(unittest.TestCase):
|
|
"""Tests for `_md_code_fence`."""
|
|
|
|
def test_default_three_backticks_when_no_backticks(self):
|
|
self.assertEqual(_md_code_fence("plain,csv,content"), "```")
|
|
|
|
def test_three_backticks_when_content_has_short_runs(self):
|
|
self.assertEqual(_md_code_fence("a `b` c"), "```")
|
|
|
|
def test_grows_beyond_triple_backtick_run(self):
|
|
self.assertEqual(_md_code_fence("before ``` after"), "````")
|
|
|
|
def test_grows_to_longest_run(self):
|
|
self.assertEqual(_md_code_fence("x ````` y"), "``````")
|
|
|
|
def test_fence_actually_wraps_content(self):
|
|
content = "with ``` inside"
|
|
fence = _md_code_fence(content)
|
|
block = f"{fence}\n{content}\n{fence}"
|
|
# The closing fence must be on its own line and not appear within
|
|
# the content, so the block is unambiguous.
|
|
self.assertNotIn(fence, content)
|
|
self.assertTrue(block.startswith(fence + "\n"))
|
|
self.assertTrue(block.endswith("\n" + fence))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|