Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions .github/workflows/binskim.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,24 @@ jobs:
$bs = (Get-ChildItem "$env:RUNNER_TEMP\bs\Microsoft.CodeAnalysis.BinSkim\tools\net*\win-x64\BinSkim.exe" | Select-Object -First 1).FullName
& $bs analyze publish-out/OmniVec.Worker.dll --output binskim-dotnet.sarif --recurse false

- name: Apply documented upstream-toolchain suppressions
run: python scripts/security/filter_binskim_sarif.py binskim-dotnet.sarif binskim-dotnet-final.sarif

- name: Upload BinSkim SARIF
if: always()
continue-on-error: true
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: binskim-dotnet.sarif
sarif_file: binskim-dotnet-final.sarif
category: binskim-dotnet

- uses: actions/upload-artifact@v4
if: always()
with:
name: binskim-dotnet-sarif
path: binskim-dotnet.sarif
path: |
binskim-dotnet.sarif
binskim-dotnet-final.sarif

binskim-go:
name: BinSkim Go CLI
Expand All @@ -84,19 +89,24 @@ jobs:
$bs = (Get-ChildItem "$env:RUNNER_TEMP\bs\Microsoft.CodeAnalysis.BinSkim\tools\net*\win-x64\BinSkim.exe" | Select-Object -First 1).FullName
& $bs analyze cli/omnivec.exe --output binskim-go.sarif --ignorePdbLoadError

- name: Apply documented upstream-toolchain suppressions
run: python scripts/security/filter_binskim_sarif.py binskim-go.sarif binskim-go-final.sarif

- name: Upload BinSkim SARIF
if: always()
continue-on-error: true
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: binskim-go.sarif
sarif_file: binskim-go-final.sarif
category: binskim-go

- uses: actions/upload-artifact@v4
if: always()
with:
name: binskim-go-sarif
path: binskim-go.sarif
path: |
binskim-go.sarif
binskim-go-final.sarif

binskim-rust:
name: BinSkim Rust router (docgrok-router)
Expand Down
7 changes: 7 additions & 0 deletions scripts/security/binskim_suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,12 @@
"binary": "docgrok-router.exe",
"reason": "Spectre mitigations (/Qspectre) are an MSVC cl.exe codegen feature. rustc / LLVM does not currently emit the IDD_VC_FEATURE Spectre marker into the PE COFF debug record, so BinSkim's BA2024 detector cannot observe the mitigation. The C runtime modules linked from libcmt.lib/libucrt.lib would also require the optional 'C++ Spectre-mitigated libs' VS workload to be installed in the build environment. Threat surface is bounded: docgrok-router does not execute user-supplied indices on shared cores in a multi-tenant manner. Re-evaluate when rust-lang/rust adds Spectre marker emission (rust-lang/rust#103956)."
}
],
"invocation_notifications": [
{
"descriptorId": "ERR997.ExceptionLoadingPdb",
"binary": "omnivec.exe",
"reason": "Go binaries built with `go build -trimpath -ldflags '-s -w'` do not emit Microsoft PDB files. BinSkim's PE/COFF analyzers cannot evaluate Go binaries — this is the documented limitation, not a tool failure. Go's security posture is enforced by the Go toolchain (govulncheck, the runtime's bounds-checking and W^X-by-default semantics) rather than the MSVC linker switches BinSkim inspects. Accepting this notification keeps the BinSkim configuration upload clean while we continue to scan Go for vulnerabilities via separate workflows."
}
]
}
65 changes: 61 additions & 4 deletions scripts/security/filter_binskim_sarif.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
from pathlib import Path


def load_suppressions(path: Path) -> list[dict]:
def load_suppressions(path: Path) -> tuple[list[dict], list[dict]]:
with path.open("r", encoding="utf-8") as fh:
data = json.load(fh)
return data.get("suppressions", [])
return data.get("suppressions", []), data.get("invocation_notifications", [])


def matches(result: dict, supp: dict) -> bool:
Expand Down Expand Up @@ -89,6 +89,61 @@ def _ensure_message_text(sarif: dict) -> int:
return patched


def _notification_matches(notif: dict, supp: dict) -> bool:
"""Match a SARIF toolConfigurationNotification against a suppression entry."""
desc_id = (notif.get("descriptor") or {}).get("id") or notif.get("ruleId")
if desc_id != supp.get("descriptorId"):
return False
binary = supp.get("binary", "")
if not binary:
return True
for loc in notif.get("locations", []) or []:
uri = (
loc.get("physicalLocation", {})
.get("artifactLocation", {})
.get("uri", "")
)
if uri.lower().endswith(binary.lower()):
return True
return False


def _normalize_invocations(sarif: dict, notif_suppressions: list[dict]) -> int:
"""Demote accepted invocation notifications to ``note`` (with audit-trail
suppression annotation) and flip ``executionSuccessful`` back to True when
no error-level notifications remain. Returns the number of notifications
that were demoted."""
demoted = 0
for run in sarif.get("runs", []) or []:
for inv in run.get("invocations", []) or []:
for notif in inv.get("toolConfigurationNotifications", []) or []:
if notif.get("level") not in ("error", "warning"):
continue
matched = next(
(s for s in notif_suppressions if _notification_matches(notif, s)),
None,
)
if not matched:
continue
notif["level"] = "note"
notif.setdefault("suppressions", []).append(
{
"kind": "external",
"status": "accepted",
"justification": matched.get("reason", ""),
}
)
demoted += 1
remaining_errors = sum(
1
for n in inv.get("toolConfigurationNotifications", []) or []
if n.get("level") in ("error", "warning")
)
if remaining_errors == 0:
inv["executionSuccessful"] = True
return demoted


def main(argv: list[str]) -> int:
if len(argv) < 3:
print(__doc__, file=sys.stderr)
Expand All @@ -101,7 +156,7 @@ def main(argv: list[str]) -> int:
else Path(__file__).with_name("binskim_suppressions.json")
)

suppressions = load_suppressions(supp_path)
suppressions, notif_suppressions = load_suppressions(supp_path)

with inp.open("r", encoding="utf-8") as fh:
sarif = json.load(fh)
Expand Down Expand Up @@ -135,6 +190,7 @@ def main(argv: list[str]) -> int:

outp.parent.mkdir(parents=True, exist_ok=True)
patched = _ensure_message_text(sarif)
notif_demoted = _normalize_invocations(sarif, notif_suppressions)
with outp.open("w", encoding="utf-8") as fh:
json.dump(sarif, fh, indent=2)

Expand All @@ -146,7 +202,8 @@ def main(argv: list[str]) -> int:
)
print(
f"BinSkim filter: kept={kept} suppressed={dropped} "
f"unsuppressed_fails_or_warnings={fails} message_text_patched={patched} -> {outp}"
f"unsuppressed_fails_or_warnings={fails} message_text_patched={patched} "
f"notifications_demoted={notif_demoted} -> {outp}"
)
return 0 if fails == 0 else 1

Expand Down
Loading