Skip to content

Commit be503bb

Browse files
authored
Merge pull request #220 from CharmingGroot/feat/tm4-k8s-privileged-workload
feat(analyzer): detect privileged Kubernetes workload deployment as TM4
2 parents 78be329 + c2522e3 commit be503bb

3 files changed

Lines changed: 92 additions & 3 deletions

File tree

‎src/skillspector/nodes/analyzers/pattern_defaults.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ class PatternCategory(StrEnum):
8282
"TM1": "Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).",
8383
"TM2": "Tool calls are chained to bypass individual safety checks or escalate capabilities beyond what any single tool call would allow.",
8484
"TM3": "Tool defaults are unsafe or overly permissive (e.g. disabled TLS verification, no authentication, world-writable permissions). Unsafe defaults widen the attack surface.",
85+
"TM4": "Code deploys a privileged Kubernetes workload (privileged container, hostPath mount, or host namespaces). This grants root on the node and is a node/cluster takeover vector.",
8586
# Rogue Agent (B.1.11)
8687
"RA1": "Skill modifies its own code, configuration, or behavior at runtime. Self-modification enables an agent to escalate privileges, disable safety constraints, or install persistent backdoors.",
8788
"RA2": "Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.",
@@ -172,6 +173,7 @@ class PatternCategory(StrEnum):
172173
"TM1": PatternCategory.TOOL_MISUSE.value,
173174
"TM2": PatternCategory.TOOL_MISUSE.value,
174175
"TM3": PatternCategory.TOOL_MISUSE.value,
176+
"TM4": PatternCategory.TOOL_MISUSE.value,
175177
"RA1": PatternCategory.ROGUE_AGENT.value,
176178
"RA2": PatternCategory.ROGUE_AGENT.value,
177179
"SC4": PatternCategory.SUPPLY_CHAIN.value,
@@ -248,6 +250,7 @@ class PatternCategory(StrEnum):
248250
"TM1": "Tool Parameter Abuse",
249251
"TM2": "Chaining Abuse",
250252
"TM3": "Unsafe Defaults",
253+
"TM4": "Privileged Kubernetes Workload",
251254
"RA1": "Self-Modification",
252255
"RA2": "Session Persistence",
253256
"SC4": "Known Vulnerable Dependency",
@@ -329,6 +332,7 @@ class PatternCategory(StrEnum):
329332
"TM1": "Validate all tool parameters against an allowlist. Reject dangerous parameter values (shell=True, --force, -rf /) and use safe defaults.",
330333
"TM2": "Limit tool chaining depth and validate the output of each tool before passing it to the next. Require explicit user approval for multi-step chains.",
331334
"TM3": "Override unsafe defaults with secure settings (verify=True, auth required, restrictive permissions). Review and harden all tool configurations.",
335+
"TM4": "Remove privileged, hostPath, and host-namespace settings from workloads. Use a least-privilege securityContext, drop capabilities, and avoid mounting the host filesystem.",
332336
# Rogue Agent (B.1.11)
333337
"RA1": "Prevent the skill from modifying its own code, SKILL.md, or configuration files. Treat skill files as read-only at runtime.",
334338
"RA2": "Remove any persistence mechanisms (cron jobs, startup scripts, state files). Skills should not maintain state across sessions without explicit user consent.",

‎src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16-
"""Static patterns: tool misuse (TM1–TM3). Node and analyze() in one module.
16+
"""Static patterns: tool misuse (TM1–TM4). Node and analyze() in one module.
1717
1818
Detects patterns where tool parameters are abused (TM1), tool chaining
19-
is used to bypass safety (TM2), or tool defaults are unsafe (TM3).
19+
is used to bypass safety (TM2), tool defaults are unsafe (TM3), or a
20+
privileged Kubernetes workload is deployed (TM4).
2021
2122
Framework: ASI02.
2223
"""
@@ -31,7 +32,7 @@
3132
from skillspector.state import AnalyzerNodeResponse, SkillspectorState
3233

3334
from . import static_runner
34-
from .common import get_context, get_line_number
35+
from .common import get_context, get_line_number, is_code_example
3536
from .pattern_defaults import PatternCategory
3637

3738
logger = get_logger(__name__)
@@ -149,6 +150,18 @@
149150
),
150151
]
151152

153+
# TM4: Privileged Kubernetes Workload — manifest/CLI primitives that grant
154+
# node/host takeover (the cluster-scale counterpart of a privileged container).
155+
# Only isolation-breaking signals are matched, so a normal `kubectl apply` or a
156+
# plain DaemonSet does not fire.
157+
TM4_PATTERNS = [
158+
(r"privileged\s*:\s*true", 0.7), # privileged container in a manifest
159+
(r"hostPath\s*:", 0.55), # host filesystem mount
160+
(r"host(?:PID|Network|IPC)\s*:\s*true", 0.6), # host namespace sharing
161+
(r"kubectl\s+run\b[^\n]*--privileged", 0.7), # privileged ad-hoc pod
162+
(r"--set\b[^\n]*privileged\s*=\s*true", 0.6), # helm privileged override
163+
]
164+
152165

153166
_SAFE_CONTAINER_PATTERNS: tuple[re.Pattern[str], ...] = (
154167
re.compile(r"docker\s+run\s+.*--rm", re.IGNORECASE),
@@ -267,6 +280,26 @@ def ctx(start: int) -> str:
267280
matched_text=match.group(0)[:200],
268281
)
269282
)
283+
# TM4: privileged K8s workload. Filtered through is_code_example() because
284+
# privileged/hostPath fields commonly appear in SKILL.md docs and examples.
285+
for pattern, confidence in TM4_PATTERNS:
286+
for match in re.finditer(pattern, content, re.IGNORECASE | re.MULTILINE):
287+
context_text = ctx(match.start())
288+
if is_code_example(context_text):
289+
continue
290+
line_num = get_line_number(content, match.start())
291+
findings.append(
292+
AnalyzerFinding(
293+
rule_id="TM4",
294+
message="Privileged Kubernetes Workload",
295+
severity=Severity.HIGH,
296+
location=loc(line_num),
297+
confidence=confidence,
298+
tags=tag,
299+
context=context_text,
300+
matched_text=match.group(0)[:200],
301+
)
302+
)
270303
return findings
271304

272305

‎tests/unit/test_patterns_new.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,58 @@ def test_tm1_rm_outside_dockerfile_stays_high(self) -> None:
684684
def test_tm3_detected(self, content: str, filename: str, filetype: str) -> None:
685685
assert any(f.rule_id == "TM3" for f in tm_mod.analyze(content, filename, filetype))
686686

687+
@pytest.mark.parametrize(
688+
"content,filename,filetype",
689+
[
690+
pytest.param(
691+
" securityContext:\n privileged: true",
692+
"daemonset.yaml",
693+
"yaml",
694+
id="privileged_true",
695+
),
696+
pytest.param(
697+
" volumes:\n - hostPath:\n path: /",
698+
"ds.yaml",
699+
"yaml",
700+
id="hostpath",
701+
),
702+
pytest.param(" hostPID: true", "ds.yaml", "yaml", id="hostpid"),
703+
pytest.param(" hostNetwork: true", "ds.yaml", "yaml", id="hostnetwork"),
704+
pytest.param(
705+
"kubectl run probe --image=alpine --privileged",
706+
"deploy.sh",
707+
"shell",
708+
id="kubectl_run_privileged",
709+
),
710+
pytest.param(
711+
"helm install m ./c --set securityContext.privileged=true",
712+
"deploy.sh",
713+
"shell",
714+
id="helm_privileged",
715+
),
716+
],
717+
)
718+
def test_tm4_detected(self, content: str, filename: str, filetype: str) -> None:
719+
assert any(f.rule_id == "TM4" for f in tm_mod.analyze(content, filename, filetype))
720+
721+
def test_tm4_severity_high(self) -> None:
722+
findings = tm_mod.analyze(
723+
" securityContext:\n privileged: true", "ds.yaml", "yaml"
724+
)
725+
tm4 = [f for f in findings if f.rule_id == "TM4"]
726+
assert tm4 and tm4[0].severity == Severity.HIGH
727+
728+
def test_tm4_benign_workload_not_flagged(self) -> None:
729+
content = (
730+
"kind: DaemonSet\nspec:\n template:\n spec:\n containers:\n"
731+
" - name: app\n image: nginx"
732+
)
733+
assert not any(f.rule_id == "TM4" for f in tm_mod.analyze(content, "ds.yaml", "yaml"))
734+
735+
def test_tm4_documentation_example_excluded(self) -> None:
736+
content = "For example, never set privileged: true in your manifests."
737+
assert not any(f.rule_id == "TM4" for f in tm_mod.analyze(content, "README.md", "markdown"))
738+
687739
def test_safe_content_produces_no_findings(self) -> None:
688740
findings = tm_mod.analyze(
689741
"import json\ndata = json.loads(input_str)", "parser.py", "python"

0 commit comments

Comments
 (0)