privmap 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. privmap-1.0.0/LICENSE +21 -0
  2. privmap-1.0.0/PKG-INFO +174 -0
  3. privmap-1.0.0/README.md +157 -0
  4. privmap-1.0.0/privmap/__init__.py +2 -0
  5. privmap-1.0.0/privmap/analysis/__init__.py +0 -0
  6. privmap-1.0.0/privmap/analysis/paths.py +49 -0
  7. privmap-1.0.0/privmap/analysis/remediation.py +148 -0
  8. privmap-1.0.0/privmap/analysis/scoring.py +138 -0
  9. privmap-1.0.0/privmap/cli.py +195 -0
  10. privmap-1.0.0/privmap/graph/__init__.py +0 -0
  11. privmap-1.0.0/privmap/graph/builder.py +86 -0
  12. privmap-1.0.0/privmap/graph/model.py +275 -0
  13. privmap-1.0.0/privmap/graph/traversal.py +242 -0
  14. privmap-1.0.0/privmap/ingestion/__init__.py +0 -0
  15. privmap-1.0.0/privmap/ingestion/capabilities.py +310 -0
  16. privmap-1.0.0/privmap/ingestion/execution.py +405 -0
  17. privmap-1.0.0/privmap/ingestion/filesystem.py +336 -0
  18. privmap-1.0.0/privmap/ingestion/identity.py +355 -0
  19. privmap-1.0.0/privmap/ingestion/processes.py +136 -0
  20. privmap-1.0.0/privmap/output/__init__.py +0 -0
  21. privmap-1.0.0/privmap/output/cli_output.py +164 -0
  22. privmap-1.0.0/privmap/output/json_export.py +38 -0
  23. privmap-1.0.0/privmap/output/markdown_export.py +89 -0
  24. privmap-1.0.0/privmap.egg-info/PKG-INFO +174 -0
  25. privmap-1.0.0/privmap.egg-info/SOURCES.txt +31 -0
  26. privmap-1.0.0/privmap.egg-info/dependency_links.txt +1 -0
  27. privmap-1.0.0/privmap.egg-info/entry_points.txt +2 -0
  28. privmap-1.0.0/privmap.egg-info/requires.txt +8 -0
  29. privmap-1.0.0/privmap.egg-info/top_level.txt +1 -0
  30. privmap-1.0.0/pyproject.toml +38 -0
  31. privmap-1.0.0/setup.cfg +4 -0
  32. privmap-1.0.0/tests/test_graph.py +167 -0
  33. privmap-1.0.0/tests/test_scoring.py +48 -0
privmap-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
privmap-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,174 @@
1
+ Metadata-Version: 2.4
2
+ Name: privmap
3
+ Version: 1.0.0
4
+ Summary: Linux privilege graph engine — model effective access, trace escalation paths.
5
+ License: MIT
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: networkx>=3.0
10
+ Requires-Dist: rich>=13.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=7.0; extra == "dev"
13
+ Requires-Dist: pytest-cov; extra == "dev"
14
+ Requires-Dist: mypy; extra == "dev"
15
+ Requires-Dist: ruff; extra == "dev"
16
+ Dynamic: license-file
17
+
18
+ # privmap
19
+ ![tests](https://github.com/isaacc2/privmap/workflows/tests/badge.svg)
20
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
21
+
22
+ Linux privilege graph engine. privmap models effective access on a Linux system as a directed graph and traces concrete privilege escalation paths through it.
23
+
24
+ ```
25
+ [CRITICAL] 2 escalation paths found for user: www-data
26
+
27
+ Path 1 — www-data → root (4 hops)
28
+ www-data
29
+ MEMBER_OF group: adm
30
+ CAN_WRITE file: /etc/logrotate.d/nginx (mode: 0664)
31
+ EXECUTES cron: /etc/cron.daily (runs-as: root)
32
+ → root
33
+
34
+ Risk: Writable logrotate config executed by root daily cron
35
+ Remediation: chmod 644 /etc/logrotate.d/nginx; chown root:root /etc/logrotate.d/nginx
36
+ ```
37
+
38
+ ## Why
39
+
40
+ Tools like LinPEAS, LinEnum, and BeRoot enumerate privilege-relevant findings as flat lists of independent observations. They report that a file is world-writable, and separately that the same file is executed by a root cron job, but they do not connect those facts into a single exploitable path. The analyst correlates the findings manually.
41
+
42
+ privmap treats this as a graph reachability problem. Every finding is a node or edge in a directed property graph. The question is not "what misconfigurations exist" but "given this user, what is reachable from here, and through what sequence of relationships."
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ pip install privmap
48
+ ```
49
+
50
+ Or from source:
51
+
52
+ ```bash
53
+ git clone https://github.com/isaacc2/privmap.git
54
+ cd privmap
55
+ pip install -e .
56
+ ```
57
+
58
+ Requires Python 3.8 or later.
59
+
60
+ ## Usage
61
+
62
+ ### Live analysis
63
+
64
+ ```bash
65
+ sudo privmap # full scan, all users
66
+ sudo privmap --user www-data --user bob # specific users
67
+ sudo privmap --min-severity high # filter by severity
68
+ sudo privmap --output json > report.json # JSON for SIEM
69
+ sudo privmap --output markdown > report.md
70
+ ```
71
+
72
+ Run as root for complete results. Without root, privmap cannot read `/etc/shadow`, walk all of `/etc`, or enumerate other users' processes, and findings will be incomplete.
73
+
74
+ ### Snapshot mode
75
+
76
+ For offline or forensic analysis, run the collector on the target:
77
+
78
+ ```bash
79
+ sudo ./collect.sh
80
+ ```
81
+
82
+ This produces `privmap_snapshot_<hostname>_<date>.tar.gz`. Transfer the archive to your analysis workstation and run:
83
+
84
+ ```bash
85
+ privmap --snapshot ./privmap_snapshot_target_20250101.tar.gz
86
+ ```
87
+
88
+ The collector is POSIX-compliant and has no runtime dependencies.
89
+
90
+ ### CI/CD integration
91
+
92
+ ```bash
93
+ privmap --exit-code --min-severity critical
94
+ ```
95
+
96
+ Returns non-zero if any path at or above the specified severity is found.
97
+
98
+ ### Other options
99
+
100
+ ```bash
101
+ --scan-paths /etc,/usr,/opt # custom filesystem scan paths
102
+ --max-depth 8 # max traversal depth (default 10)
103
+ --export-graph graph.json # dump full graph as JSON
104
+ -v / -vv # verbose / debug logging
105
+ ```
106
+
107
+ ## How it works
108
+
109
+ 1. **Ingestion.** Reads system configuration: users, groups, sudo rules, file permissions, cron jobs, systemd units, capabilities, running processes.
110
+ 2. **Graph construction.** Each finding becomes a node or edge in a directed property graph.
111
+ 3. **Reachability analysis.** DFS traversal from each non-privileged principal toward high-value sinks (root, sudo ALL, dangerous capabilities).
112
+ 4. **Semantic filtering.** Eliminates structurally invalid paths, for example a writable file that no privileged process executes.
113
+ 5. **Scoring.** Each path is scored on exploitability and impact, then assigned a severity rating.
114
+ 6. **Output.** CLI, JSON, or Markdown with per-path remediation.
115
+
116
+ ## Architecture
117
+
118
+ ```
119
+ privmap/
120
+ ├── ingestion/
121
+ │ ├── identity.py # passwd, shadow, group, sudo
122
+ │ ├── filesystem.py # permission walk, SUID, ACL
123
+ │ ├── processes.py # /proc, running services
124
+ │ ├── execution.py # cron, systemd, init.d
125
+ │ └── capabilities.py # linux capabilities, namespaces
126
+ ├── graph/
127
+ │ ├── model.py # node and edge types
128
+ │ ├── builder.py # ingestion coordinator
129
+ │ └── traversal.py # DFS reachability
130
+ ├── analysis/
131
+ │ ├── paths.py # extraction and deduplication
132
+ │ ├── scoring.py # exploitability and impact
133
+ │ └── remediation.py # per-path fix suggestions
134
+ ├── output/
135
+ │ ├── cli_output.py # rich terminal renderer
136
+ │ ├── json_export.py
137
+ │ └── markdown_export.py
138
+ └── cli.py # entry point
139
+ ```
140
+
141
+ ## Scope
142
+
143
+ privmap analyses local Linux privilege relationships. It does not perform network enumeration, run exploits, cover Windows or macOS, or replace a CVE-based vulnerability scanner. It is a structural analysis tool.
144
+
145
+ ## Known limitations
146
+
147
+ * **Argument-restricted sudo rules** receive a reduced exploitability score but are not fully validated. Rules like `sudo /usr/bin/systemctl restart nginx` may still surface as findings.
148
+ * **Capability binaries from third-party packages** are not on the known-safe allowlist and may produce false positives. The allowlist covers standard system binaries (snap-confine, ping, mtr, chronyd, and similar).
149
+ * **Snapshot mode** falls back to conservative behavior for filesystem permission checks on capability binaries. Live mode is more accurate.
150
+ * **Cron command parsing** uses regex matching on absolute paths, which can match path-like strings inside arguments or comments.
151
+ * **No CVE matching.** privmap does not check binary versions against known vulnerabilities. Use a vulnerability scanner alongside it.
152
+
153
+ ## Use cases
154
+
155
+ * **System hardening.** Validate least-privilege configurations and catch unintended escalation paths after changes.
156
+ * **Penetration testing.** Replace manual enumeration with deterministic path mapping.
157
+ * **Incident response.** Reconstruct how an attacker may have escalated privileges on a compromised host.
158
+ * **Education and CTF.** Visualise permission chains that are difficult to reason about manually.
159
+
160
+ ## Development
161
+
162
+ ```bash
163
+ pip install -e ".[dev]"
164
+ pytest tests/ -v
165
+ ruff check privmap/
166
+ ```
167
+
168
+ ## Contributing
169
+
170
+ Issues and pull requests are welcome. Run the test suite before submitting a PR. For security vulnerabilities, see [SECURITY.md](SECURITY.md).
171
+
172
+ ## License
173
+
174
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,157 @@
1
+ # privmap
2
+ ![tests](https://github.com/isaacc2/privmap/workflows/tests/badge.svg)
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
+
5
+ Linux privilege graph engine. privmap models effective access on a Linux system as a directed graph and traces concrete privilege escalation paths through it.
6
+
7
+ ```
8
+ [CRITICAL] 2 escalation paths found for user: www-data
9
+
10
+ Path 1 — www-data → root (4 hops)
11
+ www-data
12
+ MEMBER_OF group: adm
13
+ CAN_WRITE file: /etc/logrotate.d/nginx (mode: 0664)
14
+ EXECUTES cron: /etc/cron.daily (runs-as: root)
15
+ → root
16
+
17
+ Risk: Writable logrotate config executed by root daily cron
18
+ Remediation: chmod 644 /etc/logrotate.d/nginx; chown root:root /etc/logrotate.d/nginx
19
+ ```
20
+
21
+ ## Why
22
+
23
+ Tools like LinPEAS, LinEnum, and BeRoot enumerate privilege-relevant findings as flat lists of independent observations. They report that a file is world-writable, and separately that the same file is executed by a root cron job, but they do not connect those facts into a single exploitable path. The analyst correlates the findings manually.
24
+
25
+ privmap treats this as a graph reachability problem. Every finding is a node or edge in a directed property graph. The question is not "what misconfigurations exist" but "given this user, what is reachable from here, and through what sequence of relationships."
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install privmap
31
+ ```
32
+
33
+ Or from source:
34
+
35
+ ```bash
36
+ git clone https://github.com/isaacc2/privmap.git
37
+ cd privmap
38
+ pip install -e .
39
+ ```
40
+
41
+ Requires Python 3.8 or later.
42
+
43
+ ## Usage
44
+
45
+ ### Live analysis
46
+
47
+ ```bash
48
+ sudo privmap # full scan, all users
49
+ sudo privmap --user www-data --user bob # specific users
50
+ sudo privmap --min-severity high # filter by severity
51
+ sudo privmap --output json > report.json # JSON for SIEM
52
+ sudo privmap --output markdown > report.md
53
+ ```
54
+
55
+ Run as root for complete results. Without root, privmap cannot read `/etc/shadow`, walk all of `/etc`, or enumerate other users' processes, and findings will be incomplete.
56
+
57
+ ### Snapshot mode
58
+
59
+ For offline or forensic analysis, run the collector on the target:
60
+
61
+ ```bash
62
+ sudo ./collect.sh
63
+ ```
64
+
65
+ This produces `privmap_snapshot_<hostname>_<date>.tar.gz`. Transfer the archive to your analysis workstation and run:
66
+
67
+ ```bash
68
+ privmap --snapshot ./privmap_snapshot_target_20250101.tar.gz
69
+ ```
70
+
71
+ The collector is POSIX-compliant and has no runtime dependencies.
72
+
73
+ ### CI/CD integration
74
+
75
+ ```bash
76
+ privmap --exit-code --min-severity critical
77
+ ```
78
+
79
+ Returns non-zero if any path at or above the specified severity is found.
80
+
81
+ ### Other options
82
+
83
+ ```bash
84
+ --scan-paths /etc,/usr,/opt # custom filesystem scan paths
85
+ --max-depth 8 # max traversal depth (default 10)
86
+ --export-graph graph.json # dump full graph as JSON
87
+ -v / -vv # verbose / debug logging
88
+ ```
89
+
90
+ ## How it works
91
+
92
+ 1. **Ingestion.** Reads system configuration: users, groups, sudo rules, file permissions, cron jobs, systemd units, capabilities, running processes.
93
+ 2. **Graph construction.** Each finding becomes a node or edge in a directed property graph.
94
+ 3. **Reachability analysis.** DFS traversal from each non-privileged principal toward high-value sinks (root, sudo ALL, dangerous capabilities).
95
+ 4. **Semantic filtering.** Eliminates structurally invalid paths, for example a writable file that no privileged process executes.
96
+ 5. **Scoring.** Each path is scored on exploitability and impact, then assigned a severity rating.
97
+ 6. **Output.** CLI, JSON, or Markdown with per-path remediation.
98
+
99
+ ## Architecture
100
+
101
+ ```
102
+ privmap/
103
+ ├── ingestion/
104
+ │ ├── identity.py # passwd, shadow, group, sudo
105
+ │ ├── filesystem.py # permission walk, SUID, ACL
106
+ │ ├── processes.py # /proc, running services
107
+ │ ├── execution.py # cron, systemd, init.d
108
+ │ └── capabilities.py # linux capabilities, namespaces
109
+ ├── graph/
110
+ │ ├── model.py # node and edge types
111
+ │ ├── builder.py # ingestion coordinator
112
+ │ └── traversal.py # DFS reachability
113
+ ├── analysis/
114
+ │ ├── paths.py # extraction and deduplication
115
+ │ ├── scoring.py # exploitability and impact
116
+ │ └── remediation.py # per-path fix suggestions
117
+ ├── output/
118
+ │ ├── cli_output.py # rich terminal renderer
119
+ │ ├── json_export.py
120
+ │ └── markdown_export.py
121
+ └── cli.py # entry point
122
+ ```
123
+
124
+ ## Scope
125
+
126
+ privmap analyses local Linux privilege relationships. It does not perform network enumeration, run exploits, cover Windows or macOS, or replace a CVE-based vulnerability scanner. It is a structural analysis tool.
127
+
128
+ ## Known limitations
129
+
130
+ * **Argument-restricted sudo rules** receive a reduced exploitability score but are not fully validated. Rules like `sudo /usr/bin/systemctl restart nginx` may still surface as findings.
131
+ * **Capability binaries from third-party packages** are not on the known-safe allowlist and may produce false positives. The allowlist covers standard system binaries (snap-confine, ping, mtr, chronyd, and similar).
132
+ * **Snapshot mode** falls back to conservative behavior for filesystem permission checks on capability binaries. Live mode is more accurate.
133
+ * **Cron command parsing** uses regex matching on absolute paths, which can match path-like strings inside arguments or comments.
134
+ * **No CVE matching.** privmap does not check binary versions against known vulnerabilities. Use a vulnerability scanner alongside it.
135
+
136
+ ## Use cases
137
+
138
+ * **System hardening.** Validate least-privilege configurations and catch unintended escalation paths after changes.
139
+ * **Penetration testing.** Replace manual enumeration with deterministic path mapping.
140
+ * **Incident response.** Reconstruct how an attacker may have escalated privileges on a compromised host.
141
+ * **Education and CTF.** Visualise permission chains that are difficult to reason about manually.
142
+
143
+ ## Development
144
+
145
+ ```bash
146
+ pip install -e ".[dev]"
147
+ pytest tests/ -v
148
+ ruff check privmap/
149
+ ```
150
+
151
+ ## Contributing
152
+
153
+ Issues and pull requests are welcome. Run the test suite before submitting a PR. For security vulnerabilities, see [SECURITY.md](SECURITY.md).
154
+
155
+ ## License
156
+
157
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,2 @@
1
+ """privmap — Linux privilege graph engine."""
2
+ __version__ = "1.0.0"
File without changes
@@ -0,0 +1,49 @@
1
+ """Path extraction, deduplication, and filtering."""
2
+ from __future__ import annotations
3
+
4
+ from typing import List, Optional
5
+
6
+ from privmap.graph.model import EscalationPath, PrivilegeGraph, Severity
7
+ from privmap.graph.traversal import find_escalation_paths
8
+ from privmap.analysis.scoring import score_path
9
+ from privmap.analysis.remediation import generate_remediation
10
+
11
+
12
+ def analyze_paths(
13
+ graph: PrivilegeGraph,
14
+ source_users: Optional[List[str]] = None,
15
+ max_depth: int = 10,
16
+ min_severity: Severity = Severity.INFO,
17
+ ) -> List[EscalationPath]:
18
+ """Full path analysis pipeline: find, score, remediate, filter, sort."""
19
+ # Find raw paths
20
+ raw_paths = find_escalation_paths(graph, source_users, max_depth)
21
+
22
+ # Score each path
23
+ for path in raw_paths:
24
+ score_path(path)
25
+
26
+ # Generate remediation advice
27
+ for path in raw_paths:
28
+ generate_remediation(path)
29
+
30
+ # Filter by minimum severity
31
+ filtered = [p for p in raw_paths if p.severity and p.severity >= min_severity]
32
+
33
+ # Sort: CRITICAL first, then by hop count (shorter = more exploitable)
34
+ filtered.sort(
35
+ key=lambda p: (-p.severity.rank if p.severity else 0, p.hop_count)
36
+ )
37
+
38
+ return filtered
39
+
40
+
41
+ def group_paths_by_user(
42
+ paths: List[EscalationPath],
43
+ ) -> dict:
44
+ """Group escalation paths by source user."""
45
+ groups = {}
46
+ for path in paths:
47
+ user = path.source.name
48
+ groups.setdefault(user, []).append(path)
49
+ return groups
@@ -0,0 +1,148 @@
1
+ """Generate per-path remediation suggestions."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from typing import List
6
+
7
+ from privmap.graph.model import EdgeType, EscalationPath, NodeType
8
+
9
+
10
+ def generate_remediation(path: EscalationPath) -> None:
11
+ """Generate a risk description and remediation for the path. Mutates in place."""
12
+ steps: List[str] = []
13
+ risks: List[str] = []
14
+
15
+ for i, edge in enumerate(path.edges):
16
+ source_node = None
17
+ target_node = None
18
+ for n in path.nodes:
19
+ if n.id == edge.source_id:
20
+ source_node = n
21
+ if n.id == edge.target_id:
22
+ target_node = n
23
+
24
+ if edge.edge_type == EdgeType.CAN_WRITE and target_node:
25
+ file_path = target_node.properties.get("path", target_node.name)
26
+ mode = target_node.properties.get("mode", "")
27
+ reason = edge.properties.get("reason", "")
28
+
29
+ if reason == "world-writable":
30
+ risks.append(
31
+ f"World-writable file {file_path} (mode: {mode})"
32
+ )
33
+ steps.append(
34
+ f"chmod o-w {file_path}"
35
+ )
36
+ owner = target_node.properties.get("owner", "root")
37
+ steps.append(
38
+ f"chown {owner}:root {file_path}"
39
+ )
40
+ elif reason == "ACL":
41
+ risks.append(
42
+ f"ACL grants write access to {file_path}"
43
+ )
44
+ user_name = source_node.name if source_node else "user"
45
+ steps.append(
46
+ f"setfacl -x u:{user_name} {file_path}"
47
+ )
48
+ else:
49
+ risks.append(
50
+ f"Writable file {file_path} (mode: {mode})"
51
+ )
52
+ steps.append(
53
+ f"chmod 644 {file_path}; chown root:root {file_path}"
54
+ )
55
+
56
+ elif edge.edge_type == EdgeType.GRANTS:
57
+ cmd = edge.properties.get("command", "")
58
+ nopasswd = edge.properties.get("nopasswd", False)
59
+
60
+ if cmd == "ALL":
61
+ risks.append(
62
+ f"Unrestricted sudo access"
63
+ f"{' (NOPASSWD)' if nopasswd else ''}"
64
+ )
65
+ if source_node:
66
+ steps.append(
67
+ f"Restrict sudo rules for {source_node.name} to specific commands"
68
+ )
69
+ elif target_node and target_node.node_type == NodeType.SUDO_RULE:
70
+ binary = target_node.properties.get("binary", cmd)
71
+ binary_name = os.path.basename(binary) if "/" in binary else binary
72
+ risks.append(
73
+ f"Sudo rule allows {binary_name}"
74
+ f"{' (NOPASSWD)' if nopasswd else ''} — may permit shell escape"
75
+ )
76
+ # Suggest sudoedit for editors
77
+ if binary_name in ("vim", "vi", "nano", "emacs"):
78
+ steps.append(
79
+ f"Replace sudo {binary_name} rule with sudoedit"
80
+ )
81
+ else:
82
+ steps.append(
83
+ f"Remove or restrict sudo rule for {binary_name}"
84
+ )
85
+
86
+ elif edge.edge_type == EdgeType.SUID_EXEC:
87
+ if target_node:
88
+ binary = target_node.properties.get("binary", target_node.name)
89
+ path_str = target_node.properties.get("path", target_node.name)
90
+ risks.append(
91
+ f"SUID binary {binary} ({path_str}) allows privilege escalation"
92
+ )
93
+ steps.append(
94
+ f"chmod u-s {path_str}"
95
+ )
96
+ steps.append(
97
+ f"Consider using capabilities instead: setcap cap_needed+ep {path_str}"
98
+ )
99
+
100
+ elif edge.edge_type == EdgeType.HAS_CAPABILITY:
101
+ if target_node:
102
+ cap = target_node.name
103
+ binary = target_node.properties.get("binary", "")
104
+ risks.append(
105
+ f"Binary {binary} has dangerous capability {cap}"
106
+ )
107
+ steps.append(
108
+ f"setcap -r {binary}"
109
+ )
110
+ steps.append(
111
+ f"Review if {cap} is strictly necessary for {os.path.basename(binary)}"
112
+ )
113
+
114
+ elif edge.edge_type == EdgeType.EXECUTES:
115
+ if source_node and target_node:
116
+ if source_node.node_type == NodeType.CRON_JOB:
117
+ run_as = source_node.properties.get("run_as", "root")
118
+ script = target_node.properties.get("path", target_node.name)
119
+ risks.append(
120
+ f"Cron job runs {script} as {run_as}"
121
+ )
122
+ steps.append(
123
+ f"Ensure {script} is owned by root and not writable by others"
124
+ )
125
+ elif source_node.node_type == NodeType.SYSTEMD_UNIT:
126
+ unit = source_node.name
127
+ script = target_node.properties.get("path", target_node.name)
128
+ risks.append(
129
+ f"Systemd unit {unit} executes {script}"
130
+ )
131
+ steps.append(
132
+ f"Ensure {script} is owned by root with mode 755 or stricter"
133
+ )
134
+
135
+ elif edge.edge_type == EdgeType.MEMBER_OF:
136
+ if target_node:
137
+ group = target_node.name
138
+ if group in ("docker", "lxd", "disk", "adm", "shadow"):
139
+ risks.append(
140
+ f"Membership in privileged group '{group}'"
141
+ )
142
+ if source_node:
143
+ steps.append(
144
+ f"Remove {source_node.name} from group {group} unless required"
145
+ )
146
+
147
+ path.risk_description = "; ".join(risks) if risks else "Privilege escalation path detected"
148
+ path.remediation = "; ".join(steps) if steps else "Review and restrict permissions along this path"