116 lines
4.2 KiB
Python
116 lines
4.2 KiB
Python
|
|
"""STRIDE-leg tagging in the SARIF emitter (strix.report.sarif).
|
||
|
|
|
||
|
|
Every finding's SARIF rule (and, by inheritance via ``ruleId``, its results)
|
||
|
|
carries one or more ``stride:<leg>`` tags derived from the finding's CWE, so the
|
||
|
|
GitHub code-scanning Security tab and ASPM dashboards can group/filter by
|
||
|
|
threat-model leg. Unmapped or no-CWE findings fall back to a default so coverage
|
||
|
|
reports have no gaps.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from strix.report.sarif import (
|
||
|
|
_CWE_TO_STRIDE,
|
||
|
|
_DEFAULT_STRIDE_LEGS,
|
||
|
|
_stride_legs_for_cwe,
|
||
|
|
build_sarif_report,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _finding(**overrides: Any) -> dict[str, Any]:
|
||
|
|
finding: dict[str, Any] = {
|
||
|
|
"id": "vuln-0001",
|
||
|
|
"title": "Missing authentication on gRPC endpoint",
|
||
|
|
"severity": "critical",
|
||
|
|
"cwe": "CWE-306",
|
||
|
|
"description": "The gRPC server registers no auth interceptor.",
|
||
|
|
}
|
||
|
|
finding.update(overrides)
|
||
|
|
return finding
|
||
|
|
|
||
|
|
|
||
|
|
def _rule_tags(doc: dict[str, Any]) -> list[str]:
|
||
|
|
return doc["runs"][0]["tool"]["driver"]["rules"][0]["properties"]["tags"]
|
||
|
|
|
||
|
|
|
||
|
|
def test_stride_tags_on_rule_for_known_cwe() -> None:
|
||
|
|
"""CWE-306 (Missing Authentication) maps to S+E, alongside existing tags."""
|
||
|
|
tags = _rule_tags(build_sarif_report([_finding(cwe="CWE-306")]))
|
||
|
|
assert "stride:S" in tags
|
||
|
|
assert "stride:E" in tags
|
||
|
|
assert "security" in tags # existing tags preserved
|
||
|
|
assert "CWE-306" in tags
|
||
|
|
|
||
|
|
|
||
|
|
def test_stride_tags_attach_to_rule_not_duplicated_on_result() -> None:
|
||
|
|
"""STRIDE tags live on the RULE; results inherit them via ruleId (standard
|
||
|
|
SARIF) rather than duplicating — the result carries the matching ruleId and
|
||
|
|
its own strix.* properties, not a redundant tags copy."""
|
||
|
|
doc = build_sarif_report([_finding(cwe="CWE-306")])
|
||
|
|
rule = doc["runs"][0]["tool"]["driver"]["rules"][0]
|
||
|
|
result = doc["runs"][0]["results"][0]
|
||
|
|
assert result["ruleId"] == rule["id"] # inherits via ruleId
|
||
|
|
assert {"stride:S", "stride:E"} <= set(rule["properties"]["tags"])
|
||
|
|
assert "tags" not in result["properties"] # not duplicated
|
||
|
|
|
||
|
|
|
||
|
|
def test_stride_default_for_unmapped_cwe() -> None:
|
||
|
|
tags = _rule_tags(build_sarif_report([_finding(cwe="CWE-99999")]))
|
||
|
|
assert "stride:T" in tags and "stride:I" in tags
|
||
|
|
|
||
|
|
|
||
|
|
def test_stride_default_for_no_cwe() -> None:
|
||
|
|
tags = _rule_tags(build_sarif_report([_finding(cwe=None)]))
|
||
|
|
assert "stride:T" in tags and "stride:I" in tags
|
||
|
|
|
||
|
|
|
||
|
|
def test_stride_sql_injection_is_tampering_not_spoofing() -> None:
|
||
|
|
tags = _rule_tags(build_sarif_report([_finding(cwe="CWE-89")]))
|
||
|
|
assert "stride:T" in tags
|
||
|
|
assert "stride:S" not in tags # SQLi is tampering, not auth-shape
|
||
|
|
|
||
|
|
|
||
|
|
def test_stride_idor_is_elevation() -> None:
|
||
|
|
tags = _rule_tags(build_sarif_report([_finding(cwe="CWE-639")]))
|
||
|
|
assert "stride:E" in tags
|
||
|
|
|
||
|
|
|
||
|
|
def test_stride_cleartext_transmission_is_info_disclosure() -> None:
|
||
|
|
tags = _rule_tags(build_sarif_report([_finding(cwe="CWE-319")]))
|
||
|
|
assert "stride:I" in tags
|
||
|
|
|
||
|
|
|
||
|
|
def test_stride_hardcoded_credentials_is_spoofing() -> None:
|
||
|
|
"""CWE-798 (Hard-coded Credentials) is Spoofing (+ Info disclosure), not the
|
||
|
|
generic default."""
|
||
|
|
tags = _rule_tags(build_sarif_report([_finding(cwe="CWE-798")]))
|
||
|
|
assert "stride:S" in tags
|
||
|
|
assert set(_stride_legs_for_cwe("CWE-798")) != set(_DEFAULT_STRIDE_LEGS)
|
||
|
|
|
||
|
|
|
||
|
|
def test_stride_missing_authorization_is_elevation() -> None:
|
||
|
|
"""CWE-862 (Missing Authorization) is Elevation of privilege — sibling of
|
||
|
|
863 Incorrect Authorization."""
|
||
|
|
tags = _rule_tags(build_sarif_report([_finding(cwe="CWE-862")]))
|
||
|
|
assert "stride:E" in tags
|
||
|
|
assert "stride:T" not in tags # not the default
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("raw", ["CWE-306", "306", "cwe 306", "CWE306"])
|
||
|
|
def test_stride_cwe_normalisation_variants(raw: str) -> None:
|
||
|
|
"""CWE id variants all resolve to the same legs (S+E for 306)."""
|
||
|
|
tags = _rule_tags(build_sarif_report([_finding(cwe=raw)]))
|
||
|
|
assert "stride:S" in tags and "stride:E" in tags
|
||
|
|
|
||
|
|
|
||
|
|
def test_every_leg_letter_is_valid() -> None:
|
||
|
|
"""Sanity: the mapping only emits the six canonical STRIDE letters."""
|
||
|
|
valid = {"S", "T", "R", "I", "D", "E"}
|
||
|
|
for legs in _CWE_TO_STRIDE.values():
|
||
|
|
assert set(legs) <= valid, f"invalid STRIDE leg in {legs}"
|
||
|
|
assert set(_DEFAULT_STRIDE_LEGS) <= valid
|