mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-23 06:30:28 +08:00
Narrow OPP to two yes/no security questions: 1. Can data leave this sandbox? (exfiltration) 2. Can the agent write despite read-only intent? (write bypass) Removes 4 advisory/lint queries (overpermissive methods, L4 gaps, binary inheritance, inference relay), 3 output modes (Mermaid, topology, HTML), and fixes 3 bugs (include_workdir not used in exfil analysis, __all__ broken when protos missing, CLI PYTHONPATH). ~40% line reduction (2900 -> 1769 lines). Signed-off-by: Alexander Watson <zredlined@gmail.com>
117 lines
3.3 KiB
Python
117 lines
3.3 KiB
Python
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
"""Load and match accepted risk annotations against findings."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING
|
|
|
|
import yaml
|
|
|
|
from .queries import Finding
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
|
|
@dataclass
|
|
class AcceptedRisk:
|
|
query: str
|
|
reason: str
|
|
binary: str = ""
|
|
endpoint: str = ""
|
|
|
|
|
|
def load_accepted_risks(path: Path) -> list[AcceptedRisk]:
|
|
"""Load accepted risks from YAML."""
|
|
with open(path) as f: # noqa: PTH123
|
|
raw = yaml.safe_load(f)
|
|
|
|
return [
|
|
AcceptedRisk(
|
|
query=r.get("query", ""),
|
|
reason=r.get("reason", ""),
|
|
binary=r.get("binary", ""),
|
|
endpoint=r.get("endpoint", ""),
|
|
)
|
|
for r in (raw.get("accepted_risks") or [])
|
|
]
|
|
|
|
|
|
def _path_matches_risk(path, risk: AcceptedRisk) -> bool:
|
|
"""Check if a single finding path matches an accepted risk."""
|
|
if risk.binary:
|
|
path_binary = getattr(path, "binary", "")
|
|
if path_binary != risk.binary:
|
|
return False
|
|
|
|
if risk.endpoint:
|
|
endpoint_host = getattr(path, "endpoint_host", "")
|
|
if endpoint_host != risk.endpoint:
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def apply_accepted_risks(
|
|
findings: list[Finding],
|
|
accepted: list[AcceptedRisk],
|
|
) -> list[Finding]:
|
|
"""Mark findings as accepted where they match accepted risk annotations.
|
|
|
|
A finding is accepted if ALL of its paths match at least one accepted risk
|
|
entry for that query. If only some paths match, the finding stays active
|
|
with the unmatched paths.
|
|
"""
|
|
if not accepted:
|
|
return findings
|
|
|
|
result = []
|
|
for finding in findings:
|
|
matching_risks = [r for r in accepted if r.query == finding.query]
|
|
if not matching_risks:
|
|
result.append(finding)
|
|
continue
|
|
|
|
unmatched_paths = []
|
|
matched_reason = ""
|
|
for path in finding.paths:
|
|
path_accepted = False
|
|
for risk in matching_risks:
|
|
if _path_matches_risk(path, risk):
|
|
path_accepted = True
|
|
matched_reason = risk.reason
|
|
break
|
|
if not path_accepted:
|
|
unmatched_paths.append(path)
|
|
|
|
if not unmatched_paths:
|
|
accepted_finding = Finding(
|
|
query=finding.query,
|
|
title=finding.title,
|
|
description=finding.description,
|
|
risk=finding.risk,
|
|
paths=finding.paths,
|
|
remediation=finding.remediation,
|
|
accepted=True,
|
|
accepted_reason=matched_reason,
|
|
)
|
|
result.append(accepted_finding)
|
|
elif len(unmatched_paths) < len(finding.paths):
|
|
result.append(
|
|
Finding(
|
|
query=finding.query,
|
|
title=finding.title,
|
|
description=finding.description,
|
|
risk=finding.risk,
|
|
paths=unmatched_paths,
|
|
remediation=finding.remediation,
|
|
)
|
|
)
|
|
else:
|
|
result.append(finding)
|
|
|
|
return result
|