From 2b15280be3793172770ec424de5d4c9f6e25b01f Mon Sep 17 00:00:00 2001 From: CharmingGroot Date: Sun, 28 Jun 2026 22:51:41 +0900 Subject: [PATCH] feat(analyzer): detect untrusted container image pull as SC7 supply_chain (SC1-SC6) covers package dependencies but not the container-image supply chain. A skill pulling images with verification disabled (--disable-content-trust, DOCKER_CONTENT_TRUST=0, --insecure-registry) accepts tampered images but scored 9/SAFE (#223). Add SC7_PATTERNS to the supply_chain analyzer (is_code_example filter) with pattern_defaults entries and 5 tests. --tls-verify=false is excluded since TM3's verify=False already covers it. Signed-off-by: CharmingGroot --- .../nodes/analyzers/pattern_defaults.py | 4 ++ .../analyzers/static_patterns_supply_chain.py | 38 ++++++++++++-- tests/nodes/analyzers/test_static_patterns.py | 51 +++++++++++++++++++ 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/src/skillspector/nodes/analyzers/pattern_defaults.py b/src/skillspector/nodes/analyzers/pattern_defaults.py index dcece108..dad3b7a1 100644 --- a/src/skillspector/nodes/analyzers/pattern_defaults.py +++ b/src/skillspector/nodes/analyzers/pattern_defaults.py @@ -88,6 +88,7 @@ class PatternCategory(StrEnum): "SC4": "Dependency has known vulnerabilities (CVEs). Using packages with unpatched security flaws exposes the environment to known exploits.", "SC5": "Dependency appears abandoned or unmaintained. Abandoned packages no longer receive security patches, leaving known and future vulnerabilities unaddressed.", "SC6": "Package name closely resembles a popular package, suggesting possible typosquatting. Attackers publish malicious packages with similar names to trick developers into installing them.", + "SC7": "Code pulls a container image with signature or registry verification disabled (--disable-content-trust, DOCKER_CONTENT_TRUST=0, --insecure-registry). This accepts tampered or unverified images and is a container supply-chain risk.", # Trigger Abuse "TR1": "Skill uses overly broad trigger patterns that match common words or phrases, causing it to activate in unintended contexts and potentially shadow other skills.", "TR2": "Skill trigger shadows a common built-in command or another skill's trigger, potentially intercepting requests meant for trusted functionality.", @@ -175,6 +176,7 @@ class PatternCategory(StrEnum): "SC4": PatternCategory.SUPPLY_CHAIN.value, "SC5": PatternCategory.SUPPLY_CHAIN.value, "SC6": PatternCategory.SUPPLY_CHAIN.value, + "SC7": PatternCategory.SUPPLY_CHAIN.value, "TR1": PatternCategory.TRIGGER_ABUSE.value, "TR2": PatternCategory.TRIGGER_ABUSE.value, "TR3": PatternCategory.TRIGGER_ABUSE.value, @@ -250,6 +252,7 @@ class PatternCategory(StrEnum): "SC4": "Known Vulnerable Dependency", "SC5": "Abandoned Dependency", "SC6": "Typosquatting Dependency", + "SC7": "Untrusted Container Image", "TR1": "Overly Broad Trigger", "TR2": "Shadow Command Trigger", "TR3": "Keyword Baiting Trigger", @@ -332,6 +335,7 @@ class PatternCategory(StrEnum): "SC4": "Update the dependency to a patched version that addresses the known CVE. Check OSV (osv.dev) or NVD for details on the vulnerability.", "SC5": "Replace the abandoned dependency with an actively maintained alternative. Check the package's repository for last commit date and open issues.", "SC6": "Verify the package name is correct and not a typosquatting variant. Compare against the official package name on PyPI or npm.", + "SC7": "Keep image signature verification (Docker Content Trust / cosign) and registry TLS enabled. Pull only signed images from trusted registries; never disable content-trust or use insecure registries in skill code.", # Trigger Abuse "TR1": "Use specific, narrow trigger patterns that match only the skill's intended use case. Avoid single-word or common-phrase triggers.", "TR2": "Choose triggers that do not conflict with built-in commands or other skills. Prefix with a unique namespace if necessary.", diff --git a/src/skillspector/nodes/analyzers/static_patterns_supply_chain.py b/src/skillspector/nodes/analyzers/static_patterns_supply_chain.py index 3d9f8382..2240b0a3 100644 --- a/src/skillspector/nodes/analyzers/static_patterns_supply_chain.py +++ b/src/skillspector/nodes/analyzers/static_patterns_supply_chain.py @@ -13,12 +13,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Static patterns: supply chain (SC1–SC6) and trigger analysis (TR1–TR3). +"""Static patterns: supply chain (SC1–SC7) and trigger analysis (TR1–TR3). SC1–SC3: regex-based pattern matching (original implementation). SC4: Known vulnerable dependencies — live OSV.dev lookup with static fallback. SC5: Abandoned dependencies — flags known-abandoned or archived packages. SC6: Typosquatting — flags package names similar to popular packages. +SC7: Untrusted container image — flags image signature / registry-verification bypass. TR1–TR3: Trigger analysis — flags overly broad, shadowing, or baiting triggers. Node and analyze() in one module. @@ -36,7 +37,7 @@ from skillspector.state import AnalyzerNodeResponse, SkillspectorState from . import static_runner -from .common import get_context, get_line_number +from .common import get_context, get_line_number, is_code_example from .osv_client import ECOSYSTEM_NPM, ECOSYSTEM_PYPI, VulnResult, query_batch, was_osv_reachable from .pattern_defaults import PatternCategory from .static_runner import analyzer_finding_to_finding @@ -96,6 +97,17 @@ (r"decode\s+(?:this|the)\s+(?:base64|hex)\s+(?:and\s+)?(?:run|execute)", 0.8), ] +# SC7: Untrusted Container Image — pulling images with signature/registry +# verification turned off. These flags disable image trust regardless of the +# registry, so they are a strong supply-chain signal with near-zero FP. +# (`--tls-verify=false` is intentionally omitted: TM3's `verify=False` already +# covers it; SC7 targets the image-specific bypasses TM3 does not see.) +SC7_PATTERNS = [ + (r"--disable-content-trust", 0.85), # Docker Content Trust signature check off + (r"DOCKER_CONTENT_TRUST\s*=\s*0", 0.85), # signature verification disabled via env + (r"--insecure-registry", 0.8), # registry TLS verification off +] + # --------------------------------------------------------------------------- # SC4: Known Vulnerable Dependencies # @@ -504,7 +516,7 @@ def parts(v: str) -> tuple[int, ...]: def analyze(content: str, file_path: str, file_type: str) -> list[AnalyzerFinding]: - """Analyze content for supply chain patterns (SC1–SC3).""" + """Analyze content for supply chain patterns (SC1–SC3, SC7).""" findings: list[AnalyzerFinding] = [] def loc(ln: int) -> Location: @@ -573,6 +585,26 @@ def ctx(start: int) -> str: matched_text=match.group(0)[:200], ) ) + # SC7: untrusted container image. Filtered through is_code_example() because + # these flags appear in SKILL.md docs and "never do this" warnings. + for pattern, confidence in SC7_PATTERNS: + for match in re.finditer(pattern, content, re.IGNORECASE | re.MULTILINE): + context_text = ctx(match.start()) + if is_code_example(context_text): + continue + line_num = get_line_number(content, match.start()) + findings.append( + AnalyzerFinding( + rule_id="SC7", + message="Untrusted Container Image", + severity=Severity.HIGH, + location=loc(line_num), + confidence=confidence, + tags=tag, + context=context_text, + matched_text=match.group(0)[:200], + ) + ) return findings diff --git a/tests/nodes/analyzers/test_static_patterns.py b/tests/nodes/analyzers/test_static_patterns.py index b0e3454c..e74bc3b6 100644 --- a/tests/nodes/analyzers/test_static_patterns.py +++ b/tests/nodes/analyzers/test_static_patterns.py @@ -188,6 +188,57 @@ def test_sc2_curl_bash_produces_finding(self): assert len(sc2) >= 1 assert sc2[0].severity == "HIGH" + def test_sc7_disable_content_trust_produces_finding(self): + """docker pull --disable-content-trust yields SC7, HIGH severity.""" + state = { + "components": ["setup.sh"], + "file_cache": { + "setup.sh": "docker pull --disable-content-trust registry.io/base:latest" + }, + } + findings = static_runner.run_static_patterns(state, [supply_chain_module]) + sc7 = [f for f in findings if f.rule_id == "SC7"] + assert len(sc7) >= 1 + assert sc7[0].severity == "HIGH" + + def test_sc7_content_trust_env_produces_finding(self): + """DOCKER_CONTENT_TRUST=0 yields SC7.""" + state = { + "components": ["setup.sh"], + "file_cache": {"setup.sh": "export DOCKER_CONTENT_TRUST=0"}, + } + findings = static_runner.run_static_patterns(state, [supply_chain_module]) + assert any(f.rule_id == "SC7" for f in findings) + + def test_sc7_insecure_registry_produces_finding(self): + """--insecure-registry yields SC7.""" + state = { + "components": ["setup.sh"], + "file_cache": {"setup.sh": "docker pull --insecure-registry 10.0.0.5:5000/tools"}, + } + findings = static_runner.run_static_patterns(state, [supply_chain_module]) + assert any(f.rule_id == "SC7" for f in findings) + + def test_sc7_documentation_example_excluded(self): + """Verification-bypass flags in documentation do not yield SC7.""" + state = { + "components": ["README.md"], + "file_cache": { + "README.md": "For example, never use --disable-content-trust in production." + }, + } + findings = static_runner.run_static_patterns(state, [supply_chain_module]) + assert not any(f.rule_id == "SC7" for f in findings) + + def test_sc7_benign_pull_no_finding(self): + """A normal docker pull with verification on does not yield SC7.""" + state = { + "components": ["setup.sh"], + "file_cache": {"setup.sh": "docker pull nginx:1.25"}, + } + findings = static_runner.run_static_patterns(state, [supply_chain_module]) + assert not any(f.rule_id == "SC7" for f in findings) + class TestRunStaticPatternsAgentSnoopingAdditional: """run_static_patterns with agent_snooping: AS1, AS2, AS3."""