autoscan / core /scoring.py
Chris4K's picture
Upload 384 files
a2a5bfd verified
Raw
History Blame Contribute Delete
4.13 kB
"""Numeric risk scoring for individual findings.
Builds on top of the existing ``confidence`` and ``severity`` fields that
every finding already carries (set by ``make_finding()`` in ``core.models``).
Score range: 0–10 (clamped). Higher = more urgent.
"""
from __future__ import annotations
import re
from typing import Any
from core.models import CONFIDENCE_RANK, SEVERITY_RANK
# ---------------------------------------------------------------------------
# HuggingFace employee accounts — owner gets a small escalation bonus
# ---------------------------------------------------------------------------
EMPLOYEE_ACCOUNTS: frozenset[str] = frozenset({
"clem", "lysandre", "thomwolf", "julien-c", "osanseviero",
"sgugger", "victorsanh", "reach-vb", "pcuenq", "nielsr",
"patrickvonplaten", "lewtun", "narsil", "merve", "sanchit-gandhi",
})
# ---------------------------------------------------------------------------
# Score tables (mirror SEVERITY_RANK / CONFIDENCE_RANK as additive points)
# ---------------------------------------------------------------------------
_SEV_PTS: dict[str, int] = {"CRITICAL": 4, "ERROR": 3, "HIGH": 3, "WARNING": 2, "MEDIUM": 2, "INFO": 1, "LOW": 1}
_CONF_PTS: dict[str, int] = {"confirmed": 3, "likely": 2, "possible": 1}
# Regex patterns that suggest user-supplied input reaches the finding
_USER_INPUT_PATTERNS: list[re.Pattern[str]] = [
re.compile(r"request\.", re.IGNORECASE),
re.compile(r"gr\.State", re.IGNORECASE),
re.compile(r"gr\.Textbox", re.IGNORECASE),
re.compile(r"input\(", re.IGNORECASE),
re.compile(r"upload", re.IGNORECASE),
re.compile(r"user_input", re.IGNORECASE),
re.compile(r"form\.", re.IGNORECASE),
re.compile(r"query_params", re.IGNORECASE),
]
# Upload-widget triggers (gradio / streamlit)
_UPLOAD_TRIGGERS: frozenset[str] = frozenset({
"gr.File", "gr.UploadButton", "st.file_uploader",
"files.upload", "UploadedFile",
})
def _extract_owner(repo_url: str) -> str:
"""Return the HF/GitHub owner from a repo URL, or empty string."""
# Matches: https://huggingface.co/owner/name or https://github.com/owner/name
m = re.search(r"(?:huggingface\.co|github\.com)/([^/]+)", repo_url)
return m.group(1).lower() if m else ""
def score_finding(
finding: dict[str, Any],
repo_url: str,
all_findings: list[dict[str, Any]],
) -> int:
"""Compute a 0–10 risk score for *finding*.
Scoring breakdown (max 10):
- Severity: ERROR=3, WARNING=2, INFO=1
- Confidence: confirmed=3, likely=2, possible=1
- User-input path heuristic (+3 if message or file suggests user input)
- Upload widget present (+2 if severity is ERROR and scan has an upload widget finding)
- Employee owner (+1 if HF employee owns the space)
- MCP present (+1 if scan contains MCP-related findings)
"""
sev = finding.get("severity", "INFO").upper()
conf = finding.get("confidence", "possible").lower()
score = _SEV_PTS.get(sev, 1) + _CONF_PTS.get(conf, 1)
# User-input heuristic: message or file path suggests user-controlled data
message = finding.get("message", "")
file_path = finding.get("file", "")
combined = f"{message} {file_path}"
if any(p.search(combined) for p in _USER_INPUT_PATTERNS):
score += 3
# Upload widget escalation: only meaningful when combined with ERROR severity
if sev == "ERROR":
has_upload = any(
any(tok in f.get("message", "") or tok in f.get("file", "") for tok in _UPLOAD_TRIGGERS)
for f in all_findings
)
if has_upload:
score += 2
# Employee account bonus
owner = _extract_owner(repo_url)
if owner and owner in EMPLOYEE_ACCOUNTS:
score += 1
# MCP presence: escalate if the scan contains MCP agent findings
has_mcp = any(
"mcp" in f.get("rule", "").lower() or "mcp" in f.get("message", "").lower()
for f in all_findings
)
if has_mcp:
score += 1
return min(score, 10)