Skip to content
Open
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
4 changes: 4 additions & 0 deletions src/skillspector/nodes/analyzers/pattern_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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.",
Expand Down
32 changes: 30 additions & 2 deletions src/skillspector/nodes/analyzers/static_patterns_supply_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
#
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -573,6 +585,22 @@ def ctx(start: int) -> str:
matched_text=match.group(0)[:200],
)
)
# SC7: untrusted container image. Example filtering is delegated to the runner.
for pattern, confidence in SC7_PATTERNS:
for match in re.finditer(pattern, content, re.IGNORECASE | re.MULTILINE):
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=ctx(match.start()),
matched_text=match.group(0)[:200],
)
)
return findings


Expand Down
66 changes: 66 additions & 0 deletions tests/nodes/analyzers/test_static_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,72 @@ 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)

def test_sc7_example_marker_in_executable_still_fires(self):
"""An 'example' marker near a bypass in an executable .sh must NOT suppress SC7.

Example filtering belongs to the runner, which only downweights (does not
skip) executables — so a nearby '# for example' cannot be used to evade SC7.
"""
state = {
"components": ["setup.sh"],
"file_cache": {
"setup.sh": "# for example\ndocker pull --disable-content-trust registry.io/x",
},
}
findings = static_runner.run_static_patterns(state, [supply_chain_module])
assert any(f.rule_id == "SC7" for f in findings)


class TestRunStaticPatternsAgentSnoopingAdditional:
"""run_static_patterns with agent_snooping: AS1, AS2, AS3."""
Expand Down