weblate/fuzzing/findings_reporter.py
Michal Čihař 1826011030 chore: migrate ruff suppressions to human-readable names
These are easier to understand when reading the code. The feature is in
preview mode since ruff 0.15.17.
2026-06-14 21:56:11 +02:00

917 lines
28 KiB
Python
Vendored

# Copyright © Weblate contributors
#
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import annotations
import argparse
import json
import logging
import os
import re
import sys
import time
import uuid
from dataclasses import dataclass
from html import escape as html_escape
from pathlib import Path
from typing import TYPE_CHECKING, Any
from urllib.parse import urlsplit
from urllib.request import Request, urlopen
if TYPE_CHECKING:
from collections.abc import Mapping
LOGGER = logging.getLogger(__name__)
DEFAULT_SARIF_PATH = Path("cifuzz-sarif/results.sarif")
DEFAULT_ARTIFACTS_PATH = Path("out/artifacts")
MAX_SUMMARY_LENGTH = 8000
MAX_CONTEXT_LENGTH = 512
MAX_REPORT_SUMMARY_LENGTH = 8000
MAX_REPORT_SUMMARY_LINES = 120
MAX_REPORT_ARTIFACT_FILES = 20
UNKNOWN = "unknown"
_ASAN_RE = re.compile(r"ERROR:\s*([^:\n]+):\s*([^\n]+)")
_SUMMARY_RE = re.compile(r"SUMMARY:\s*([^:\n]+):\s*([^\n]+)")
_PYTHON_EXCEPTION_RE = re.compile(
r"^([A-Za-z_][\w.]*?(?:Error|Exception|Warning|Interrupt|Exit))(?:\s*:\s*(.*))?$",
re.MULTILINE,
)
_PYTHON_EXCEPTION_RULE_IDS = frozenset(
{
"python-exception",
"python_exception",
"python exception",
}
)
@dataclass(slots=True)
class CrashSummary:
path: Path
target: str
text: str
crash_type: str
@dataclass(slots=True)
class FuzzFinding:
source: str
rule_id: str
target: str
message: str
crash_type: str
location: str
summary: CrashSummary | None = None
@dataclass(slots=True)
class ReportConfig:
dsn: str
mode: str
sanitizer: str
environment: str
release: str
workflow: str
run_id: str
run_attempt: str
sha: str
repository: str
run_url: str
artifact_names: tuple[str, ...]
@dataclass(slots=True)
class ParsedDsn:
public_key: str
endpoint: str
def truncate(value: str, limit: int = MAX_CONTEXT_LENGTH) -> str:
if len(value) <= limit:
return value
return f"{value[:limit]}... [truncated]"
def clean_tag(value: str) -> str:
return truncate(value or UNKNOWN, 200)
def extract_crash_type(text: str) -> str:
for regex in (_ASAN_RE, _SUMMARY_RE, _PYTHON_EXCEPTION_RE):
match = regex.search(text)
if match:
return truncate(": ".join(group for group in match.groups() if group))
for line in text.splitlines():
line = line.strip()
if line:
return truncate(line)
return UNKNOWN
def is_python_exception_text(text: str) -> bool:
return bool(_PYTHON_EXCEPTION_RE.search(text))
def is_python_exception_rule(rule_id: str) -> bool:
normalized = rule_id.lower().replace("-", " ").replace("_", " ")
return normalized in _PYTHON_EXCEPTION_RULE_IDS or (
"python" in normalized and "exception" in normalized
)
def is_directly_captured_python_exception(
result: dict[str, Any], message: str, summary: CrashSummary | None
) -> bool:
rule_id = result.get("ruleId", "")
if isinstance(rule_id, str) and is_python_exception_rule(rule_id):
return True
text = message if summary is None else f"{message}\n{summary.text}"
return is_python_exception_text(text)
def summary_target(path: Path, artifacts_path: Path) -> str:
try:
relative = path.relative_to(artifacts_path)
except ValueError:
return UNKNOWN
return relative.parts[0] if relative.parts else UNKNOWN
def collect_crash_summaries(artifacts_path: Path) -> list[CrashSummary]:
if not artifacts_path.exists():
return []
summaries = []
for summary_path in sorted(artifacts_path.glob("**/*.summary")):
text = summary_path.read_text(errors="replace")
summaries.append(
CrashSummary(
path=summary_path,
target=summary_target(summary_path, artifacts_path),
text=text,
crash_type=extract_crash_type(text),
)
)
return summaries
def message_text(result: dict[str, Any]) -> str:
message = result.get("message")
if isinstance(message, dict):
text = message.get("text") or message.get("markdown")
if isinstance(text, str):
return text
return result.get("ruleId", UNKNOWN)
def physical_location(location: dict[str, Any]) -> tuple[str, int | None]:
physical = location.get("physicalLocation")
if not isinstance(physical, dict):
return "", None
artifact = physical.get("artifactLocation")
uri = artifact.get("uri", "") if isinstance(artifact, dict) else ""
region = physical.get("region")
line = region.get("startLine") if isinstance(region, dict) else None
return uri, line if isinstance(line, int) else None
def iter_result_locations(result: dict[str, Any]) -> list[tuple[str, int | None]]:
locations = [
physical_location(location)
for location in result.get("locations", [])
if isinstance(location, dict)
]
for stack in result.get("stacks", []):
if not isinstance(stack, dict):
continue
for frame in stack.get("frames", []):
if not isinstance(frame, dict):
continue
location = frame.get("location")
if isinstance(location, dict):
locations.append(physical_location(location))
return [(uri, line) for uri, line in locations if uri]
def format_location(result: dict[str, Any]) -> str:
for uri, line in iter_result_locations(result):
if line is None:
return uri
return f"{uri}:{line}"
return UNKNOWN
def target_from_uri(uri: str) -> str:
marker = "out/artifacts/"
if marker not in uri:
return UNKNOWN
target = uri.split(marker, 1)[1].split("/", 1)[0]
return target or UNKNOWN
def target_from_result(result: dict[str, Any]) -> str:
properties = result.get("properties")
if isinstance(properties, dict):
for key in ("target", "fuzzTarget", "fuzz_target", "fuzzer"):
value = properties.get(key)
if isinstance(value, str) and value:
return value
for uri, _line in iter_result_locations(result):
target = target_from_uri(uri)
if target != UNKNOWN:
return target
return UNKNOWN
def normalized_location_uri(uri: str) -> str:
parsed = urlsplit(uri)
path = parsed.path if parsed.scheme else uri
return path.replace("\\", "/").strip("/")
def path_contains_segment(path: str, segment: str) -> bool:
normalized_path = f"/{path.strip('/')}/"
normalized_segment = f"/{segment.strip('/')}/"
return normalized_segment in normalized_path
def summary_relative_path(summary: CrashSummary, artifacts_path: Path) -> str:
try:
return summary.path.relative_to(artifacts_path).as_posix()
except ValueError:
return summary.path.as_posix()
def summary_matches_result(
summary: CrashSummary, result: dict[str, Any], artifacts_path: Path
) -> bool:
relative_path = summary_relative_path(summary, artifacts_path)
relative_parent = Path(relative_path).parent.as_posix()
if relative_parent == "." or relative_parent.count("/") < 1:
relative_parent = ""
for uri, _line in iter_result_locations(result):
normalized_uri = normalized_location_uri(uri)
if path_contains_segment(normalized_uri, relative_path):
return True
if relative_parent and path_contains_segment(normalized_uri, relative_parent):
return True
return False
def matching_summary(
result: dict[str, Any], summaries: list[CrashSummary], artifacts_path: Path
) -> CrashSummary | None:
for summary in summaries:
if summary_matches_result(summary, result, artifacts_path):
return summary
return None
def collect_sarif_findings(
sarif_path: Path, summaries: list[CrashSummary], artifacts_path: Path
) -> list[FuzzFinding]:
if not sarif_path.exists():
return []
with sarif_path.open(encoding="utf-8") as handle:
sarif_data = json.load(handle)
findings = []
for run in sarif_data.get("runs", []):
if not isinstance(run, dict):
continue
for result in run.get("results", []):
if not isinstance(result, dict):
continue
target = target_from_result(result)
matched_summary = matching_summary(result, summaries, artifacts_path)
message = message_text(result)
if is_directly_captured_python_exception(result, message, matched_summary):
continue
findings.append(
FuzzFinding(
source="sarif",
rule_id=result.get("ruleId", UNKNOWN),
target=target,
message=message,
crash_type=extract_crash_type(
f"{message}\n{matched_summary.text if matched_summary else ''}"
),
location=format_location(result),
summary=matched_summary,
)
)
return findings
def collect_findings(
sarif_path: Path, artifacts_path: Path
) -> tuple[list[FuzzFinding], list[CrashSummary]]:
summaries = collect_crash_summaries(artifacts_path)
sarif_findings = collect_sarif_findings(sarif_path, summaries, artifacts_path)
sarif_summary_paths = {
finding.summary.path for finding in sarif_findings if finding.summary
}
summary_findings = [
FuzzFinding(
source="summary",
rule_id=summary.path.stem,
target=summary.target,
message=summary.crash_type,
crash_type=summary.crash_type,
location=UNKNOWN,
summary=summary,
)
for summary in summaries
if summary.path not in sarif_summary_paths
if not is_python_exception_text(summary.text)
]
return deduplicate_findings([*sarif_findings, *summary_findings]), summaries
def deduplicate_findings(findings: list[FuzzFinding]) -> list[FuzzFinding]:
seen: set[tuple[str, str, str, str, str]] = set()
deduplicated = []
for finding in findings:
key = finding_key(finding)
if key in seen:
continue
seen.add(key)
deduplicated.append(finding)
return deduplicated
def finding_key(finding: FuzzFinding) -> tuple[str, str, str, str, str]:
return (
finding.source,
finding.rule_id,
finding.target,
fingerprint_crash_type(finding.crash_type),
fingerprint_location(finding.location),
)
def fingerprint_crash_type(crash_type: str) -> str:
if not crash_type or crash_type == UNKNOWN:
return UNKNOWN
return crash_type.split(":", 1)[0]
def fingerprint_location(location: str) -> str:
if location.startswith("out/artifacts/"):
return UNKNOWN
return location or UNKNOWN
def count_sarif_results(sarif_path: Path) -> int | None:
if not sarif_path.exists():
return None
with sarif_path.open(encoding="utf-8") as handle:
sarif_data = json.load(handle)
count = 0
for run in sarif_data.get("runs", []):
if not isinstance(run, dict):
continue
results = run.get("results", [])
if isinstance(results, list):
count += len(results)
return count
def display_path(path: Path) -> str:
try:
return path.relative_to(Path.cwd()).as_posix()
except ValueError:
return path.as_posix()
def display_value(value: str) -> str:
return value if value and value != UNKNOWN else UNKNOWN
def report_source_label(finding: FuzzFinding) -> str:
if finding.source == "sarif" and finding.summary:
return "SARIF + crash summary"
if finding.source == "sarif":
return "SARIF"
if finding.source == "summary":
return "crash summary"
return finding.source
def summary_excerpt_text(text: str) -> str:
text = text.strip()
if not text:
return "(empty)"
lines = text.splitlines()
excerpt = "\n".join(lines[:MAX_REPORT_SUMMARY_LINES])
truncated = len(lines) > MAX_REPORT_SUMMARY_LINES
if len(excerpt) > MAX_REPORT_SUMMARY_LENGTH:
excerpt = excerpt[:MAX_REPORT_SUMMARY_LENGTH]
truncated = True
if truncated:
excerpt = f"{excerpt}\n... [truncated]"
return excerpt
def format_summary_excerpt(text: str) -> list[str]:
return [f" {line}" for line in summary_excerpt_text(text).splitlines()]
def summary_artifact_files(summary: CrashSummary) -> list[Path]:
directory = summary.path.parent
if not directory.exists():
return []
return sorted(path for path in directory.iterdir() if path.is_file())
def append_summary_report(lines: list[str], summary: CrashSummary) -> None:
lines.extend(
(
f"Summary file: {display_path(summary.path)}",
f"Artifact directory: {display_path(summary.path.parent)}",
)
)
artifact_files = summary_artifact_files(summary)
if artifact_files:
lines.append("Artifact files:")
lines.extend(
f" - {artifact_file.name}"
for artifact_file in artifact_files[:MAX_REPORT_ARTIFACT_FILES]
)
if len(artifact_files) > MAX_REPORT_ARTIFACT_FILES:
lines.append(
f" - ... {len(artifact_files) - MAX_REPORT_ARTIFACT_FILES} more"
)
lines.append("Summary excerpt:")
lines.extend(format_summary_excerpt(summary.text))
def format_findings_report(
findings: list[FuzzFinding],
summaries: list[CrashSummary],
*,
sarif_path: Path = DEFAULT_SARIF_PATH,
artifacts_path: Path = DEFAULT_ARTIFACTS_PATH,
) -> str:
sarif_results = count_sarif_results(sarif_path)
lines = [
"Fuzzing report",
"==============",
f"SARIF results: {sarif_results if sarif_results is not None else 'not generated'}",
f"Artifact root: {display_path(artifacts_path)}",
f"Crash summary files: {len(summaries)}",
f"Findings: {len(findings)}",
]
if not findings and not summaries:
lines.extend(("", "No fuzz findings found."))
return "\n".join(lines)
for index, finding in enumerate(findings, start=1):
source = report_source_label(finding)
lines.extend(
(
"",
f"Finding {index}",
"-" * len(f"Finding {index}"),
f"Target: {display_value(finding.target)}",
f"Source: {source}",
f"Rule: {display_value(finding.rule_id)}",
f"Crash: {display_value(finding.crash_type)}",
f"Likely location: {display_value(finding.location)}",
)
)
if finding.message and finding.message != finding.crash_type:
lines.append(f"Message: {truncate(finding.message, MAX_CONTEXT_LENGTH)}")
if finding.summary:
append_summary_report(lines, finding.summary)
finding_summary_paths = {
finding.summary.path for finding in findings if finding.summary is not None
}
additional_summaries = [
summary for summary in summaries if summary.path not in finding_summary_paths
]
if additional_summaries:
lines.extend(("", "Additional crash summaries", "--------------------------"))
for index, summary in enumerate(additional_summaries, start=1):
lines.extend(
("", f"Summary {index}", f"Target: {display_value(summary.target)}")
)
lines.append(f"Crash: {display_value(summary.crash_type)}")
append_summary_report(lines, summary)
return "\n".join(lines)
def markdown_text(value: str) -> str:
return html_escape(display_value(value))
def markdown_code(value: str) -> str:
value = display_value(value).replace("\n", " ")
if value == UNKNOWN:
return UNKNOWN
if "`" in value:
return f"`` {html_escape(value)} ``"
return f"`{html_escape(value)}`"
def markdown_code_block(value: str) -> str:
value = summary_excerpt_text(value)
longest_backtick_run = max(
(len(match.group(0)) for match in re.finditer(r"`+", value)),
default=0,
)
fence = "`" * max(3, longest_backtick_run + 1)
return f"{fence}text\n{value}\n{fence}"
def append_summary_markdown(lines: list[str], summary: CrashSummary) -> None:
lines.extend(
(
f"- Summary file: {markdown_code(display_path(summary.path))}",
f"- Artifact directory: {markdown_code(display_path(summary.path.parent))}",
)
)
artifact_files = summary_artifact_files(summary)
if artifact_files:
lines.extend(("", "Artifact files:"))
lines.extend(
f"- {markdown_code(artifact_file.name)}"
for artifact_file in artifact_files[:MAX_REPORT_ARTIFACT_FILES]
)
if len(artifact_files) > MAX_REPORT_ARTIFACT_FILES:
lines.append(
f"- ... {len(artifact_files) - MAX_REPORT_ARTIFACT_FILES} more"
)
lines.extend(
(
"",
"<details>",
"<summary>Summary excerpt</summary>",
"",
markdown_code_block(summary.text),
"",
"</details>",
)
)
def format_findings_step_summary(
findings: list[FuzzFinding],
summaries: list[CrashSummary],
*,
sarif_path: Path = DEFAULT_SARIF_PATH,
artifacts_path: Path = DEFAULT_ARTIFACTS_PATH,
) -> str:
sarif_results = count_sarif_results(sarif_path)
lines = [
"## Fuzzing report",
"",
f"- SARIF results: {sarif_results if sarif_results is not None else 'not generated'}",
f"- Artifact root: {markdown_code(display_path(artifacts_path))}",
f"- Crash summary files: {len(summaries)}",
f"- Findings: {len(findings)}",
]
if not findings and not summaries:
lines.extend(("", "No fuzz findings found."))
return "\n".join(lines)
for index, finding in enumerate(findings, start=1):
lines.extend(
(
"",
f"### Finding {index}",
"",
f"- Target: {markdown_code(finding.target)}",
f"- Source: {markdown_text(report_source_label(finding))}",
f"- Rule: {markdown_code(finding.rule_id)}",
f"- Crash: {markdown_text(finding.crash_type)}",
f"- Likely location: {markdown_code(finding.location)}",
)
)
if finding.message and finding.message != finding.crash_type:
lines.append(
f"- Message: {markdown_text(truncate(finding.message, MAX_CONTEXT_LENGTH))}"
)
if finding.summary:
append_summary_markdown(lines, finding.summary)
finding_summary_paths = {
finding.summary.path for finding in findings if finding.summary is not None
}
additional_summaries = [
summary for summary in summaries if summary.path not in finding_summary_paths
]
if additional_summaries:
lines.extend(("", "### Additional crash summaries"))
for index, summary in enumerate(additional_summaries, start=1):
lines.extend(
(
"",
f"#### Summary {index}",
"",
f"- Target: {markdown_code(summary.target)}",
f"- Crash: {markdown_text(summary.crash_type)}",
)
)
append_summary_markdown(lines, summary)
return "\n".join(lines)
def write_github_step_summary(
markdown_report: str, environ: Mapping[str, str] | None = None
) -> bool:
environ = os.environ if environ is None else environ
summary_path = environ.get("GITHUB_STEP_SUMMARY")
if not summary_path:
LOGGER.info("GITHUB_STEP_SUMMARY is not configured; skipping step summary.")
return False
with Path(summary_path).open("a", encoding="utf-8") as handle:
handle.write(markdown_report.rstrip())
handle.write("\n")
return True
def build_run_url(environ: Mapping[str, str]) -> str:
repository = environ.get("GITHUB_REPOSITORY", "")
run_id = environ.get("GITHUB_RUN_ID", "")
if not repository or not run_id:
return ""
server_url = environ.get("GITHUB_SERVER_URL", "https://github.com")
return f"{server_url}/{repository}/actions/runs/{run_id}"
def build_report_config(
environ: Mapping[str, str],
*,
mode: str,
sanitizer: str,
environment: str,
) -> ReportConfig:
artifact_prefix = f"cflite-{mode}"
return ReportConfig(
dsn=environ.get("SENTRY_DSN", ""),
mode=mode,
sanitizer=sanitizer,
environment=environment,
release=environ.get("GITHUB_SHA", ""),
workflow=environ.get("GITHUB_WORKFLOW", ""),
run_id=environ.get("GITHUB_RUN_ID", ""),
run_attempt=environ.get("GITHUB_RUN_ATTEMPT", ""),
sha=environ.get("GITHUB_SHA", ""),
repository=environ.get("GITHUB_REPOSITORY", ""),
run_url=build_run_url(environ),
artifact_names=(f"{artifact_prefix}-crash-summaries-{sanitizer}",),
)
def event_message(finding: FuzzFinding, config: ReportConfig) -> str:
target = f" in {finding.target}" if finding.target != UNKNOWN else ""
detail = finding.crash_type if finding.crash_type != UNKNOWN else finding.message
return truncate(
f"ClusterFuzzLite {config.mode} finding{target}: {detail}",
MAX_CONTEXT_LENGTH,
)
def build_event(finding: FuzzFinding, config: ReportConfig) -> dict[str, Any]:
tags = {
"source": "clusterfuzzlite",
"mode": config.mode,
"sanitizer": config.sanitizer,
"target": finding.target,
"workflow": config.workflow,
"run_id": config.run_id,
"run_attempt": config.run_attempt,
"sha": config.sha,
}
clean_tags = {key: clean_tag(value) for key, value in tags.items() if value}
extra: dict[str, Any] = {
"sarif_message": truncate(finding.message),
"artifact_names": list(config.artifact_names),
}
if finding.summary:
extra.update(
{
"summary_path": str(finding.summary.path),
"summary": truncate(finding.summary.text, MAX_SUMMARY_LENGTH),
}
)
return {
"level": "error",
"event_id": uuid.uuid4().hex,
"timestamp": time.time(),
"message": event_message(finding, config),
"release": config.release or None,
"environment": config.environment,
"fingerprint": [
"clusterfuzzlite",
config.sanitizer or UNKNOWN,
finding.target or UNKNOWN,
fingerprint_crash_type(finding.crash_type) or finding.rule_id or UNKNOWN,
fingerprint_location(finding.location),
],
"tags": clean_tags,
"contexts": {
"clusterfuzzlite": {
"source": finding.source,
"mode": config.mode,
"sanitizer": config.sanitizer,
"target": finding.target,
"rule_id": finding.rule_id,
"crash_type": finding.crash_type,
"location": finding.location,
},
"github_actions": {
"repository": config.repository,
"workflow": config.workflow,
"run_id": config.run_id,
"run_attempt": config.run_attempt,
"sha": config.sha,
"run_url": config.run_url,
},
},
"extra": extra,
}
def report_findings(findings: list[FuzzFinding], config: ReportConfig) -> int:
if not findings:
LOGGER.info("No fuzz findings to report to Sentry.")
return 0
if not config.dsn:
LOGGER.info("SENTRY_DSN is not configured; skipping Sentry fuzz reporting.")
return 0
reported = 0
for finding in findings:
try:
send_sentry_event(build_event(finding, config), config.dsn)
except (OSError, ValueError) as error:
LOGGER.warning("Could not report fuzz finding to Sentry: %s", error)
continue
reported += 1
return reported
def parse_dsn(dsn: str) -> ParsedDsn:
parsed = urlsplit(dsn)
if parsed.scheme not in {"http", "https"}:
msg = f"Unsupported Sentry DSN scheme: {parsed.scheme}"
raise ValueError(msg)
if not parsed.username:
msg = "Sentry DSN is missing the public key."
raise ValueError(msg)
path = parsed.path.rstrip("/")
project_id = path.rsplit("/", 1)[-1]
path_prefix = path[: -len(project_id)].rstrip("/")
endpoint = (
f"{parsed.scheme}://{parsed.hostname}"
f"{f':{parsed.port}' if parsed.port else ''}"
f"{path_prefix}/api/{project_id}/envelope/"
)
return ParsedDsn(public_key=parsed.username, endpoint=endpoint)
def build_envelope(event: dict[str, Any], dsn: str) -> tuple[str, bytes]:
parsed = parse_dsn(dsn)
envelope = "\n".join(
(
json.dumps({"event_id": event["event_id"]}),
json.dumps({"type": "event"}),
json.dumps(event),
)
).encode()
return parsed.public_key, envelope
def send_sentry_event(event: dict[str, Any], dsn: str) -> None:
parsed = parse_dsn(dsn)
_public_key, envelope = build_envelope(event, dsn)
# ruff: ignore[suspicious-url-open-usage]
request = Request(
parsed.endpoint,
data=envelope,
headers={
"Content-Type": "application/x-sentry-envelope",
"User-Agent": "weblate-fuzzing-sentry-reporter",
"X-Sentry-Auth": (
"Sentry sentry_version=7, "
"sentry_client=weblate-fuzzing-sentry-reporter, "
f"sentry_key={parsed.public_key}"
),
},
method="POST",
)
# ruff: ignore[suspicious-url-open-usage]
with urlopen(request, timeout=10) as response:
response.read()
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Report ClusterFuzzLite findings to the console and Sentry."
)
parser.add_argument("--sarif", type=Path, default=DEFAULT_SARIF_PATH)
parser.add_argument("--artifacts", type=Path, default=DEFAULT_ARTIFACTS_PATH)
parser.add_argument("--mode", default=os.environ.get("CFLITE_MODE", "batch"))
parser.add_argument("--sanitizer", default=os.environ.get("SANITIZER", "address"))
parser.add_argument("--environment", default="")
parser.add_argument(
"--print-report",
action="store_true",
help="Print a human-readable findings report to stdout.",
)
parser.add_argument(
"--github-step-summary",
action="store_true",
help="Append a Markdown findings report to GITHUB_STEP_SUMMARY.",
)
parser.add_argument(
"--no-sentry",
action="store_true",
help="Skip reporting findings to Sentry.",
)
parser.add_argument("--log-level", default="INFO")
return parser
def main(argv: list[str] | None = None) -> int:
parser = build_arg_parser()
args = parser.parse_args(argv)
logging.basicConfig(level=args.log_level)
environment = args.environment or f"fuzzing-{args.mode}"
config = build_report_config(
os.environ,
mode=args.mode,
sanitizer=args.sanitizer,
environment=environment,
)
findings, summaries = collect_findings(args.sarif, args.artifacts)
if args.print_report:
sys.stdout.write(
format_findings_report(
findings,
summaries,
sarif_path=args.sarif,
artifacts_path=args.artifacts,
)
+ "\n"
)
if args.github_step_summary:
write_github_step_summary(
format_findings_step_summary(
findings,
summaries,
sarif_path=args.sarif,
artifacts_path=args.artifacts,
)
)
reported = 0 if args.no_sentry else report_findings(findings, config)
LOGGER.info(
"Processed %d crash summary file(s), %d finding(s), reported %d event(s).",
len(summaries),
len(findings),
reported,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())