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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Bug Fixes

- keep same-fragment command/network correlations and relayed fail-closed findings active across Hugging Face whitelists
- validate Joblib NumPy wrapper state and scan resumed pickle opcodes around raw array payloads without flagging inert array bytes.
- bound CatBoost command-evidence redaction work and cover distant, shell-concatenated, wrapped, and netrc curl credentials
- use credential-free, cross-platform cloud routing filenames without changing native object-key suffix semantics
Expand Down
2 changes: 1 addition & 1 deletion modelaudit/cache/optimized_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from pathlib import Path
from typing import Any

CACHE_SCHEMA_VERSION = "2.1"
CACHE_SCHEMA_VERSION = "2.2"

_CACHE_ONLY_CONFIG_KEYS = frozenset(
{
Expand Down
31 changes: 25 additions & 6 deletions modelaudit/scanners/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@
"S509",
}
)
_WHITELIST_DOWNGRADE_EXEMPT_CHECK_NAMES: Final[frozenset[str]] = frozenset(
{
"Command/Network Correlation Check",
Comment thread
mldangelo-oai marked this conversation as resolved.
"RKNN Command and Network Indicator Correlation",
}
)
_WHITELIST_DOWNGRADE_EXEMPT_CORRELATION_DETAIL = "same_fragment_correlation"
# Word-boundary matching prevents incidental substrings (e.g. "executable" inside
# "ExecuTorch", "rce" inside "force") from suppressing whitelist downgrades.
_WHITELIST_DOWNGRADE_EXEMPT_KEYWORD_PATTERN: Final[re.Pattern[str]] = re.compile(
Expand Down Expand Up @@ -256,6 +263,12 @@ def _whitelist_downgrade_exempt(
if details.get("cve_id") or details.get("cve"):
return True

if (
check_name in _WHITELIST_DOWNGRADE_EXEMPT_CHECK_NAMES
and details.get(_WHITELIST_DOWNGRADE_EXEMPT_CORRELATION_DETAIL) is True
):
return True

if rule_code and (rule_code.startswith("S2") or rule_code in _WHITELIST_DOWNGRADE_EXEMPT_RULE_CODES):
return True

Expand All @@ -281,7 +294,7 @@ def _should_apply_whitelist(
Returns:
True if the issue should be downgraded to INFO, False otherwise
"""
# Only downgrade severities higher than INFO
# Only downgrade severities higher than INFO.
if severity in (IssueSeverity.INFO, IssueSeverity.DEBUG):
return False

Expand Down Expand Up @@ -335,6 +348,17 @@ def _apply_whitelist_downgrade(
Tuple of (potentially modified severity, details dict)
"""
original_severity = severity
details = dict(details or {})
if severity in (IssueSeverity.INFO, IssueSeverity.DEBUG):
return severity, details

has_whitelist_metadata = (
details.get("whitelist_downgrade") is True or details.get("whitelist_downgrade_restored") is True
)
if has_whitelist_metadata:
details.pop("whitelist_downgrade", None)
details.pop("whitelist_downgrade_restored", None)
details.pop("original_severity", None)
if self._should_apply_whitelist(
severity,
details=details,
Expand All @@ -344,13 +368,8 @@ def _apply_whitelist_downgrade(
check_name=check_name,
):
severity = IssueSeverity.INFO
# Add note about whitelisting to the details
if details is None:
details = {}
details["whitelist_downgrade"] = True
details["original_severity"] = original_severity.name
elif details is None:
details = {}

return severity, details

Expand Down
32 changes: 28 additions & 4 deletions modelaudit/scanners/catboost_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -674,16 +674,24 @@ def _analyze_text_fragments(self, fragments: list[dict[str, str]], result: ScanR
script_matches: list[dict[str, str]] = []
encoded_matches: list[dict[str, str]] = []

for fragment in fragments:
for fragment_index, fragment in enumerate(fragments):
text = fragment["text"]
lowered = text.lower().strip()
fragment_id = str(fragment_index)

if lowered in _BENIGN_METADATA_KEYS:
continue

for pattern, reason in _COMMAND_PATTERNS:
if pattern.search(text):
command_matches.append({"text": text, "section": fragment["section"], "pattern": reason})
command_matches.append(
{
"text": text,
"section": fragment["section"],
"pattern": reason,
"fragment_id": fragment_id,
},
)
break

if _PROCESS_CONTEXT_PATTERN.search(text):
Expand All @@ -692,6 +700,7 @@ def _analyze_text_fragments(self, fragments: list[dict[str, str]], result: ScanR
"text": text,
"section": fragment["section"],
"pattern": "shell/process context",
"fragment_id": fragment_id,
},
)

Expand All @@ -703,7 +712,12 @@ def _analyze_text_fragments(self, fragments: list[dict[str, str]], result: ScanR

if any(keyword in lowered_url for keyword in _SUSPICIOUS_NETWORK_KEYWORDS):
network_matches.append(
{"text": url, "section": fragment["section"], "pattern": "suspicious network URL"},
{
"text": url,
"section": fragment["section"],
"pattern": "suspicious network URL",
"fragment_id": fragment_id,
},
)

for ip_pattern in (_IPV4_PATTERN, _IPV6_PATTERN):
Expand All @@ -719,7 +733,14 @@ def _analyze_text_fragments(self, fragments: list[dict[str, str]], result: ScanR
or ip_obj.is_unspecified
):
continue
network_matches.append({"text": candidate, "section": fragment["section"], "pattern": "public IP"})
network_matches.append(
{
"text": candidate,
"section": fragment["section"],
"pattern": "public IP",
"fragment_id": fragment_id,
},
)

for pattern, reason in _SCRIPT_PATTERNS:
if pattern.search(text):
Expand Down Expand Up @@ -847,6 +868,8 @@ def _analyze_text_fragments(self, fragments: list[dict[str, str]], result: ScanR

if command_matches and (process_context_matches or network_matches):
context_matches = process_context_matches + network_matches
command_fragment_ids = {match["fragment_id"] for match in command_matches}
context_fragment_ids = {match["fragment_id"] for match in context_matches}
result.add_check(
name="Command/Network Correlation Check",
passed=False,
Expand All @@ -858,6 +881,7 @@ def _analyze_text_fragments(self, fragments: list[dict[str, str]], result: ScanR
"context_examples": self._summarize_matches(context_matches),
"command_match_count": len(command_matches),
"context_match_count": len(context_matches),
"same_fragment_correlation": bool(command_fragment_ids & context_fragment_ids),
},
why="Correlated command primitives plus process/network context strongly indicate exploit intent.",
)
Expand Down
3 changes: 3 additions & 0 deletions modelaudit/scanners/lightgbm_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,8 @@ def _analyze_lines(self, lines: list[str], result: ScanResult, path: str) -> Non
)

if critical_command_hits and network_hits:
command_lines = {hit["line"] for hit in critical_command_hits}
network_lines = {hit["line"] for hit in network_hits}
result.add_check(
name="Command/Network Correlation Check",
passed=False,
Expand All @@ -364,6 +366,7 @@ def _analyze_lines(self, lines: list[str], result: ScanResult, path: str) -> Non
details={
"command_examples": critical_command_hits[:3],
"network_examples": network_hits[:3],
"same_fragment_correlation": bool(command_lines & network_lines),
},
why="Combined command execution and external endpoint indicators raise exploitation confidence.",
)
Expand Down
6 changes: 5 additions & 1 deletion modelaudit/scanners/rknn_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,11 @@ def _check_command_and_network_indicators(
message="Correlated command execution and network indicators detected in RKNN metadata text",
severity=IssueSeverity.CRITICAL,
location=path,
details={"examples": command_network_hits[:5], "signal_type": "command_and_network"},
details={
"examples": command_network_hits[:5],
"signal_type": "command_and_network",
"same_fragment_correlation": True,
},
)
elif command_hits:
result.add_check(
Expand Down
8 changes: 8 additions & 0 deletions tests/cache/test_optimized_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@
from pathlib import Path

from modelaudit.cache.optimized_config import (
CACHE_SCHEMA_VERSION,
CacheConfiguration,
ConfigurationExtractor,
build_cache_version_context,
get_config_extractor,
)


def test_cache_schema_invalidates_pre_whitelist_policy_entries() -> None:
"""Security-policy changes must not reuse cached pre-fix severities."""
assert CACHE_SCHEMA_VERSION == "2.2"
assert build_cache_version_context()["cache_schema_version"] == "2.2"


class TestCacheConfiguration:
"""Tests for CacheConfiguration class."""

Expand Down
Loading
Loading