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.
- privmap-1.0.0/LICENSE +21 -0
- privmap-1.0.0/PKG-INFO +174 -0
- privmap-1.0.0/README.md +157 -0
- privmap-1.0.0/privmap/__init__.py +2 -0
- privmap-1.0.0/privmap/analysis/__init__.py +0 -0
- privmap-1.0.0/privmap/analysis/paths.py +49 -0
- privmap-1.0.0/privmap/analysis/remediation.py +148 -0
- privmap-1.0.0/privmap/analysis/scoring.py +138 -0
- privmap-1.0.0/privmap/cli.py +195 -0
- privmap-1.0.0/privmap/graph/__init__.py +0 -0
- privmap-1.0.0/privmap/graph/builder.py +86 -0
- privmap-1.0.0/privmap/graph/model.py +275 -0
- privmap-1.0.0/privmap/graph/traversal.py +242 -0
- privmap-1.0.0/privmap/ingestion/__init__.py +0 -0
- privmap-1.0.0/privmap/ingestion/capabilities.py +310 -0
- privmap-1.0.0/privmap/ingestion/execution.py +405 -0
- privmap-1.0.0/privmap/ingestion/filesystem.py +336 -0
- privmap-1.0.0/privmap/ingestion/identity.py +355 -0
- privmap-1.0.0/privmap/ingestion/processes.py +136 -0
- privmap-1.0.0/privmap/output/__init__.py +0 -0
- privmap-1.0.0/privmap/output/cli_output.py +164 -0
- privmap-1.0.0/privmap/output/json_export.py +38 -0
- privmap-1.0.0/privmap/output/markdown_export.py +89 -0
- privmap-1.0.0/privmap.egg-info/PKG-INFO +174 -0
- privmap-1.0.0/privmap.egg-info/SOURCES.txt +31 -0
- privmap-1.0.0/privmap.egg-info/dependency_links.txt +1 -0
- privmap-1.0.0/privmap.egg-info/entry_points.txt +2 -0
- privmap-1.0.0/privmap.egg-info/requires.txt +8 -0
- privmap-1.0.0/privmap.egg-info/top_level.txt +1 -0
- privmap-1.0.0/pyproject.toml +38 -0
- privmap-1.0.0/setup.cfg +4 -0
- privmap-1.0.0/tests/test_graph.py +167 -0
- 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
|
+

|
|
20
|
+
[](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).
|
privmap-1.0.0/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# privmap
|
|
2
|
+

|
|
3
|
+
[](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).
|
|
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"
|