exploitgraph 1.0.0__tar.gz → 1.0.2__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.
- {exploitgraph-1.0.0/exploitgraph.egg-info → exploitgraph-1.0.2}/PKG-INFO +1 -1
- exploitgraph-1.0.2/exploitgraph/data/wordlists/backup_files.txt +23 -0
- exploitgraph-1.0.2/exploitgraph/data/wordlists/common_paths.txt +149 -0
- exploitgraph-1.0.2/exploitgraph/data/wordlists/s3_buckets.txt +38 -0
- exploitgraph-1.0.2/exploitgraph/data/wordlists/subdomains.txt +31 -0
- {exploitgraph-1.0.0 → exploitgraph-1.0.2/exploitgraph.egg-info}/PKG-INFO +1 -1
- exploitgraph-1.0.2/exploitgraph.egg-info/SOURCES.txt +14 -0
- exploitgraph-1.0.2/exploitgraph.egg-info/top_level.txt +1 -0
- {exploitgraph-1.0.0 → exploitgraph-1.0.2}/pyproject.toml +3 -4
- exploitgraph-1.0.0/core/__init__.py +0 -0
- exploitgraph-1.0.0/core/attack_graph.py +0 -83
- exploitgraph-1.0.0/core/aws_client.py +0 -284
- exploitgraph-1.0.0/core/config.py +0 -83
- exploitgraph-1.0.0/core/console.py +0 -469
- exploitgraph-1.0.0/core/context_engine.py +0 -172
- exploitgraph-1.0.0/core/correlator.py +0 -476
- exploitgraph-1.0.0/core/http_client.py +0 -243
- exploitgraph-1.0.0/core/logger.py +0 -97
- exploitgraph-1.0.0/core/module_loader.py +0 -69
- exploitgraph-1.0.0/core/risk_engine.py +0 -47
- exploitgraph-1.0.0/core/session_manager.py +0 -254
- exploitgraph-1.0.0/exploitgraph.egg-info/SOURCES.txt +0 -46
- exploitgraph-1.0.0/exploitgraph.egg-info/top_level.txt +0 -2
- exploitgraph-1.0.0/modules/__init__.py +0 -0
- exploitgraph-1.0.0/modules/base.py +0 -82
- exploitgraph-1.0.0/modules/cloud/__init__.py +0 -0
- exploitgraph-1.0.0/modules/cloud/aws_credential_validator.py +0 -340
- exploitgraph-1.0.0/modules/cloud/azure_enum.py +0 -289
- exploitgraph-1.0.0/modules/cloud/cloudtrail_analyzer.py +0 -494
- exploitgraph-1.0.0/modules/cloud/gcp_enum.py +0 -272
- exploitgraph-1.0.0/modules/cloud/iam_enum.py +0 -321
- exploitgraph-1.0.0/modules/cloud/iam_privilege_escalation.py +0 -515
- exploitgraph-1.0.0/modules/cloud/metadata_check.py +0 -315
- exploitgraph-1.0.0/modules/cloud/s3_enum.py +0 -469
- exploitgraph-1.0.0/modules/discovery/__init__.py +0 -0
- exploitgraph-1.0.0/modules/discovery/http_enum.py +0 -235
- exploitgraph-1.0.0/modules/discovery/subdomain_enum.py +0 -260
- exploitgraph-1.0.0/modules/exploitation/__init__.py +0 -0
- exploitgraph-1.0.0/modules/exploitation/api_exploit.py +0 -403
- exploitgraph-1.0.0/modules/exploitation/jwt_attack.py +0 -346
- exploitgraph-1.0.0/modules/exploitation/ssrf_scanner.py +0 -258
- exploitgraph-1.0.0/modules/reporting/__init__.py +0 -0
- exploitgraph-1.0.0/modules/reporting/html_report.py +0 -446
- exploitgraph-1.0.0/modules/reporting/json_export.py +0 -107
- exploitgraph-1.0.0/modules/secrets/__init__.py +0 -0
- exploitgraph-1.0.0/modules/secrets/file_secrets.py +0 -358
- exploitgraph-1.0.0/modules/secrets/git_secrets.py +0 -267
- {exploitgraph-1.0.0 → exploitgraph-1.0.2}/LICENSE +0 -0
- {exploitgraph-1.0.0 → exploitgraph-1.0.2}/README.md +0 -0
- {exploitgraph-1.0.0 → exploitgraph-1.0.2}/exploitgraph.egg-info/dependency_links.txt +0 -0
- {exploitgraph-1.0.0 → exploitgraph-1.0.2}/exploitgraph.egg-info/entry_points.txt +0 -0
- {exploitgraph-1.0.0 → exploitgraph-1.0.2}/exploitgraph.egg-info/requires.txt +0 -0
- {exploitgraph-1.0.0 → exploitgraph-1.0.2}/setup.cfg +0 -0
- {exploitgraph-1.0.0 → exploitgraph-1.0.2}/tests/test_exploitgraph.py +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# ExploitGraph - Backup File Paths
|
|
2
|
+
/static/backups/
|
|
3
|
+
/backups/
|
|
4
|
+
/backup/
|
|
5
|
+
/.env
|
|
6
|
+
/.env.backup
|
|
7
|
+
/.env.production
|
|
8
|
+
/.env.local
|
|
9
|
+
/config.json
|
|
10
|
+
/config.yaml
|
|
11
|
+
/app.yaml
|
|
12
|
+
/database.yml
|
|
13
|
+
/Dockerfile
|
|
14
|
+
/docker-compose.yml
|
|
15
|
+
/docker-compose.yaml
|
|
16
|
+
/package.json
|
|
17
|
+
/.git/config
|
|
18
|
+
/wp-config.php
|
|
19
|
+
/web.config
|
|
20
|
+
/settings.py
|
|
21
|
+
/settings.php
|
|
22
|
+
/configuration.php
|
|
23
|
+
/config.php
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# ExploitGraph - Common HTTP Paths Wordlist
|
|
2
|
+
# Generic paths for endpoint enumeration against any web application
|
|
3
|
+
# Format: one path per line, lines starting with # are comments
|
|
4
|
+
|
|
5
|
+
# Root and API bases
|
|
6
|
+
/
|
|
7
|
+
/api
|
|
8
|
+
/api/v1
|
|
9
|
+
/api/v2
|
|
10
|
+
/api/v3
|
|
11
|
+
/v1
|
|
12
|
+
/v2
|
|
13
|
+
|
|
14
|
+
# Documentation
|
|
15
|
+
/docs
|
|
16
|
+
/api/docs
|
|
17
|
+
/swagger
|
|
18
|
+
/swagger.json
|
|
19
|
+
/swagger-ui.html
|
|
20
|
+
/swagger-ui/
|
|
21
|
+
/openapi.json
|
|
22
|
+
/openapi.yaml
|
|
23
|
+
/redoc
|
|
24
|
+
/api-docs
|
|
25
|
+
/api/swagger
|
|
26
|
+
/apidocs
|
|
27
|
+
|
|
28
|
+
# Authentication
|
|
29
|
+
/login
|
|
30
|
+
/auth
|
|
31
|
+
/auth/login
|
|
32
|
+
/api/auth/login
|
|
33
|
+
/api/login
|
|
34
|
+
/api/auth
|
|
35
|
+
/oauth
|
|
36
|
+
/oauth/token
|
|
37
|
+
/token
|
|
38
|
+
/api/token
|
|
39
|
+
/signin
|
|
40
|
+
/api/signin
|
|
41
|
+
|
|
42
|
+
# User / Account
|
|
43
|
+
/api/users
|
|
44
|
+
/api/user
|
|
45
|
+
/api/me
|
|
46
|
+
/api/account
|
|
47
|
+
/api/profile
|
|
48
|
+
/users
|
|
49
|
+
/account
|
|
50
|
+
|
|
51
|
+
# Admin
|
|
52
|
+
/admin
|
|
53
|
+
/admin/
|
|
54
|
+
/api/admin
|
|
55
|
+
/api/admin/users
|
|
56
|
+
/api/admin/config
|
|
57
|
+
/dashboard
|
|
58
|
+
/console
|
|
59
|
+
/management
|
|
60
|
+
/api/management
|
|
61
|
+
/panel
|
|
62
|
+
/control
|
|
63
|
+
/backend
|
|
64
|
+
|
|
65
|
+
# Health / Status
|
|
66
|
+
/health
|
|
67
|
+
/api/health
|
|
68
|
+
/status
|
|
69
|
+
/ping
|
|
70
|
+
/api/status
|
|
71
|
+
/api/ping
|
|
72
|
+
/metrics
|
|
73
|
+
/api/metrics
|
|
74
|
+
/actuator
|
|
75
|
+
/actuator/health
|
|
76
|
+
/actuator/env
|
|
77
|
+
/actuator/beans
|
|
78
|
+
/actuator/mappings
|
|
79
|
+
/actuator/info
|
|
80
|
+
|
|
81
|
+
# Debug / Config (often left enabled accidentally)
|
|
82
|
+
/debug
|
|
83
|
+
/api/debug
|
|
84
|
+
/api/debug/config
|
|
85
|
+
/config
|
|
86
|
+
/api/config
|
|
87
|
+
/settings
|
|
88
|
+
/api/settings
|
|
89
|
+
/env
|
|
90
|
+
/api/env
|
|
91
|
+
/__debug__
|
|
92
|
+
/api/internal
|
|
93
|
+
|
|
94
|
+
# Cloud Storage
|
|
95
|
+
/static/
|
|
96
|
+
/static/backups/
|
|
97
|
+
/backups/
|
|
98
|
+
/backup/
|
|
99
|
+
/uploads/
|
|
100
|
+
/files/
|
|
101
|
+
/storage/
|
|
102
|
+
/assets/
|
|
103
|
+
/media/
|
|
104
|
+
|
|
105
|
+
# Exposed files
|
|
106
|
+
/.env
|
|
107
|
+
/.env.production
|
|
108
|
+
/.env.local
|
|
109
|
+
/.env.backup
|
|
110
|
+
/.env.example
|
|
111
|
+
/config.json
|
|
112
|
+
/config.yaml
|
|
113
|
+
/config.yml
|
|
114
|
+
/app.yaml
|
|
115
|
+
/app.json
|
|
116
|
+
/settings.json
|
|
117
|
+
/secrets.json
|
|
118
|
+
|
|
119
|
+
# Git exposure
|
|
120
|
+
/.git/config
|
|
121
|
+
/.git/HEAD
|
|
122
|
+
/.git/COMMIT_EDITMSG
|
|
123
|
+
|
|
124
|
+
# Common backup filenames
|
|
125
|
+
/backup.zip
|
|
126
|
+
/backup.tar.gz
|
|
127
|
+
/backup.sql
|
|
128
|
+
/dump.sql
|
|
129
|
+
/db.sql
|
|
130
|
+
/database.sql
|
|
131
|
+
/site.zip
|
|
132
|
+
/app.zip
|
|
133
|
+
/source.zip
|
|
134
|
+
|
|
135
|
+
# Web server files
|
|
136
|
+
/robots.txt
|
|
137
|
+
/sitemap.xml
|
|
138
|
+
/.htaccess
|
|
139
|
+
/.htpasswd
|
|
140
|
+
/web.config
|
|
141
|
+
/phpinfo.php
|
|
142
|
+
/server-status
|
|
143
|
+
/server-info
|
|
144
|
+
/info.php
|
|
145
|
+
|
|
146
|
+
# AWS / Cloud specific
|
|
147
|
+
/latest/meta-data/
|
|
148
|
+
/latest/user-data
|
|
149
|
+
/?list-type=2
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# ExploitGraph - S3 Bucket Name Wordlist
|
|
2
|
+
# Common bucket naming patterns used by organizations
|
|
3
|
+
backups
|
|
4
|
+
backup
|
|
5
|
+
assets
|
|
6
|
+
static
|
|
7
|
+
uploads
|
|
8
|
+
files
|
|
9
|
+
data
|
|
10
|
+
logs
|
|
11
|
+
config
|
|
12
|
+
configs
|
|
13
|
+
media
|
|
14
|
+
images
|
|
15
|
+
public
|
|
16
|
+
private
|
|
17
|
+
dev
|
|
18
|
+
staging
|
|
19
|
+
prod
|
|
20
|
+
production
|
|
21
|
+
test
|
|
22
|
+
demo
|
|
23
|
+
archive
|
|
24
|
+
exports
|
|
25
|
+
reports
|
|
26
|
+
documents
|
|
27
|
+
docs
|
|
28
|
+
storage
|
|
29
|
+
cdn
|
|
30
|
+
web
|
|
31
|
+
app
|
|
32
|
+
api
|
|
33
|
+
secrets
|
|
34
|
+
credentials
|
|
35
|
+
keys
|
|
36
|
+
database
|
|
37
|
+
db
|
|
38
|
+
dump
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# ExploitGraph - Common Subdomain Wordlist
|
|
2
|
+
api
|
|
3
|
+
dev
|
|
4
|
+
staging
|
|
5
|
+
test
|
|
6
|
+
admin
|
|
7
|
+
dashboard
|
|
8
|
+
portal
|
|
9
|
+
app
|
|
10
|
+
mobile
|
|
11
|
+
secure
|
|
12
|
+
vpn
|
|
13
|
+
mail
|
|
14
|
+
smtp
|
|
15
|
+
ftp
|
|
16
|
+
cdn
|
|
17
|
+
assets
|
|
18
|
+
static
|
|
19
|
+
backup
|
|
20
|
+
files
|
|
21
|
+
upload
|
|
22
|
+
docs
|
|
23
|
+
wiki
|
|
24
|
+
support
|
|
25
|
+
help
|
|
26
|
+
status
|
|
27
|
+
monitor
|
|
28
|
+
logs
|
|
29
|
+
metrics
|
|
30
|
+
internal
|
|
31
|
+
corp
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
exploitgraph.egg-info/PKG-INFO
|
|
5
|
+
exploitgraph.egg-info/SOURCES.txt
|
|
6
|
+
exploitgraph.egg-info/dependency_links.txt
|
|
7
|
+
exploitgraph.egg-info/entry_points.txt
|
|
8
|
+
exploitgraph.egg-info/requires.txt
|
|
9
|
+
exploitgraph.egg-info/top_level.txt
|
|
10
|
+
exploitgraph/data/wordlists/backup_files.txt
|
|
11
|
+
exploitgraph/data/wordlists/common_paths.txt
|
|
12
|
+
exploitgraph/data/wordlists/s3_buckets.txt
|
|
13
|
+
exploitgraph/data/wordlists/subdomains.txt
|
|
14
|
+
tests/test_exploitgraph.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
exploitgraph
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "exploitgraph"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.2"
|
|
8
8
|
description = "Automated attack path discovery and exploitation framework for cloud-native applications"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -50,9 +50,8 @@ Repository = "https://github.com/prajwalpawar/ExploitGraph"
|
|
|
50
50
|
"Bug Tracker" = "https://github.com/prajwalpawar/ExploitGraph/issues"
|
|
51
51
|
Documentation = "https://github.com/prajwalpawar/ExploitGraph/wiki"
|
|
52
52
|
|
|
53
|
-
[tool.setuptools
|
|
54
|
-
|
|
55
|
-
include = ["exploitgraph*", "core*", "modules*"]
|
|
53
|
+
[tool.setuptools]
|
|
54
|
+
packages = ["exploitgraph"]
|
|
56
55
|
|
|
57
56
|
[tool.setuptools.package-data]
|
|
58
57
|
"*" = ["data/wordlists/*.txt", "data/templates/*.j2", "*.yaml", "*.yml"]
|
|
File without changes
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
"""ExploitGraph - Attack path graph engine (networkx)."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
import json
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
5
|
-
import networkx as nx
|
|
6
|
-
if TYPE_CHECKING:
|
|
7
|
-
from core.session_manager import Session
|
|
8
|
-
|
|
9
|
-
SEVERITY_COLOR = {"CRITICAL":"#dc2626","HIGH":"#ea580c","MEDIUM":"#d97706",
|
|
10
|
-
"LOW":"#16a34a","INFO":"#2563eb"}
|
|
11
|
-
MITRE_REF = {
|
|
12
|
-
"T1595":"Active Scanning", "T1595.003":"Wordlist Scanning",
|
|
13
|
-
"T1580":"Cloud Infrastructure Discovery", "T1530":"Data from Cloud Storage",
|
|
14
|
-
"T1552.001":"Credentials in Files", "T1552.004":"Private Keys",
|
|
15
|
-
"T1552.005":"Cloud Instance Metadata API", "T1078":"Valid Accounts",
|
|
16
|
-
"T1078.004":"Valid Accounts: Cloud Accounts", "T1548":"Abuse Elevation Control",
|
|
17
|
-
"T1550.001":"Application Access Token", "T1069.003":"Cloud Groups",
|
|
18
|
-
"T1110.001":"Password Guessing",
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class AttackGraph:
|
|
23
|
-
def __init__(self): self.G: nx.DiGraph = nx.DiGraph()
|
|
24
|
-
|
|
25
|
-
def build(self, session: "Session"):
|
|
26
|
-
self.G.clear()
|
|
27
|
-
for n in session.graph_nodes:
|
|
28
|
-
self.G.add_node(n["node_id"], label=n.get("label",""),
|
|
29
|
-
type=n.get("node_type","asset"),
|
|
30
|
-
severity=n.get("severity","INFO"),
|
|
31
|
-
details=n.get("details",""),
|
|
32
|
-
color=SEVERITY_COLOR.get(n.get("severity","INFO"),"#6b7280"))
|
|
33
|
-
for e in session.graph_edges:
|
|
34
|
-
if e["source"] in self.G and e["target"] in self.G:
|
|
35
|
-
self.G.add_edge(e["source"], e["target"],
|
|
36
|
-
label=e.get("label",""), technique=e.get("technique",""))
|
|
37
|
-
|
|
38
|
-
def find_paths(self, src="attacker", tgt="compromise") -> list[list[str]]:
|
|
39
|
-
try:
|
|
40
|
-
nodes = list(self.G.nodes())
|
|
41
|
-
src = next((n for n in nodes if src in n), src)
|
|
42
|
-
tgt = next((n for n in nodes if tgt in n.lower()), tgt)
|
|
43
|
-
return list(nx.all_simple_paths(self.G, src, tgt))
|
|
44
|
-
except Exception:
|
|
45
|
-
return []
|
|
46
|
-
|
|
47
|
-
def stats(self) -> dict:
|
|
48
|
-
techs = set()
|
|
49
|
-
for _,_,d in self.G.edges(data=True):
|
|
50
|
-
if d.get("technique"): techs.add(d["technique"])
|
|
51
|
-
critical = [n for n,d in self.G.nodes(data=True) if d.get("severity") in ("CRITICAL","HIGH")]
|
|
52
|
-
return dict(nodes=self.G.number_of_nodes(), edges=self.G.number_of_edges(),
|
|
53
|
-
critical_nodes=len(critical), mitre_techniques=sorted(techs),
|
|
54
|
-
is_connected=nx.is_weakly_connected(self.G) if self.G.nodes() else False)
|
|
55
|
-
|
|
56
|
-
def to_json(self) -> dict:
|
|
57
|
-
nodes = [dict(id=nid, label=d.get("label",nid), type=d.get("type","asset"),
|
|
58
|
-
severity=d.get("severity","INFO"), details=d.get("details",""),
|
|
59
|
-
color=d.get("color","#6b7280"))
|
|
60
|
-
for nid,d in self.G.nodes(data=True)]
|
|
61
|
-
edges = [dict(source=s, target=t, label=d.get("label",""), technique=d.get("technique",""))
|
|
62
|
-
for s,t,d in self.G.edges(data=True)]
|
|
63
|
-
return dict(nodes=nodes, edges=edges, stats=self.stats(), attack_paths=self.find_paths())
|
|
64
|
-
|
|
65
|
-
def print_ascii(self) -> str:
|
|
66
|
-
paths = self.find_paths()
|
|
67
|
-
if not paths: return "No complete attack path found."
|
|
68
|
-
path = max(paths, key=len)
|
|
69
|
-
lines = []
|
|
70
|
-
colors = {"CRITICAL":"\033[91m","HIGH":"\033[93m","MEDIUM":"\033[96m","INFO":"\033[97m"}
|
|
71
|
-
for i, nid in enumerate(path):
|
|
72
|
-
d = self.G.nodes.get(nid, {})
|
|
73
|
-
label = d.get("label", nid).split("\n")[0]
|
|
74
|
-
sev = d.get("severity","INFO")
|
|
75
|
-
if i > 0:
|
|
76
|
-
ed = self.G.edges.get((path[i-1], nid), {})
|
|
77
|
-
tech = ed.get("technique","")
|
|
78
|
-
lines.append(f" ↓ {tech}")
|
|
79
|
-
lines.append(f"{colors.get(sev,'')} [{sev}] {label}\033[0m")
|
|
80
|
-
return "\n".join(lines)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
attack_graph = AttackGraph()
|
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
ExploitGraph - AWS Client Factory
|
|
3
|
-
Production-grade boto3 session management with:
|
|
4
|
-
- Automatic retry with exponential backoff
|
|
5
|
-
- Region fallback (us-east-1 → us-west-2 → eu-west-1)
|
|
6
|
-
- Credential injection from session secrets
|
|
7
|
-
- Standardised response parsing
|
|
8
|
-
- GuardDuty detection-awareness tagging
|
|
9
|
-
|
|
10
|
-
All modules call get_client() — no per-module boto3 setup needed.
|
|
11
|
-
"""
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
import time
|
|
14
|
-
import functools
|
|
15
|
-
from typing import TYPE_CHECKING, Any, Optional
|
|
16
|
-
|
|
17
|
-
if TYPE_CHECKING:
|
|
18
|
-
from core.session_manager import Session
|
|
19
|
-
|
|
20
|
-
try:
|
|
21
|
-
import boto3
|
|
22
|
-
import botocore.config
|
|
23
|
-
import botocore.exceptions
|
|
24
|
-
HAS_BOTO3 = True
|
|
25
|
-
except ImportError:
|
|
26
|
-
HAS_BOTO3 = False
|
|
27
|
-
|
|
28
|
-
DEFAULT_REGION = "us-east-1"
|
|
29
|
-
FALLBACK_REGIONS = ["us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1"]
|
|
30
|
-
|
|
31
|
-
# Retry config: 3 attempts, exponential backoff
|
|
32
|
-
_RETRY_CONFIG = None
|
|
33
|
-
if HAS_BOTO3:
|
|
34
|
-
_RETRY_CONFIG = botocore.config.Config(
|
|
35
|
-
retries={"max_attempts": 3, "mode": "adaptive"},
|
|
36
|
-
connect_timeout=10,
|
|
37
|
-
read_timeout=20,
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
# GuardDuty-awareness: these API calls are known to trigger alerts
|
|
41
|
-
GUARDDUTY_NOISY_APIS = {
|
|
42
|
-
"GetCallerIdentity": "HIGH", # Recon — always logged
|
|
43
|
-
"GetAccountAuthorizationDetails": "CRITICAL", # Full IAM dump — very noisy
|
|
44
|
-
"ListUsers": "MEDIUM",
|
|
45
|
-
"ListRoles": "MEDIUM",
|
|
46
|
-
"CreateAccessKey": "HIGH", # Backdoor credential
|
|
47
|
-
"DeleteTrail": "CRITICAL", # Covering tracks
|
|
48
|
-
"StopLogging": "CRITICAL",
|
|
49
|
-
"PutBucketLogging": "HIGH",
|
|
50
|
-
"DescribeInstances": "LOW", # Normal ops but logged
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
GUARDDUTY_STEALTHY_APIS = {
|
|
54
|
-
"ListBuckets": "no-sign-request avoids auth logs",
|
|
55
|
-
"GetObject": "anonymous S3 access not in CloudTrail",
|
|
56
|
-
"GetBucketAcl": "no-sign-request avoids auth logs",
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def get_client(service: str,
|
|
61
|
-
session: "Optional[Session]" = None,
|
|
62
|
-
region: str = DEFAULT_REGION,
|
|
63
|
-
profile: str = "",
|
|
64
|
-
access_key: str = "",
|
|
65
|
-
secret_key: str = "",
|
|
66
|
-
session_token: str = "") -> Any:
|
|
67
|
-
"""
|
|
68
|
-
Return a boto3 client. Credential resolution order:
|
|
69
|
-
1. Explicit access_key/secret_key args
|
|
70
|
-
2. Session secrets (injected by file_secrets / cloudtrail_analyzer)
|
|
71
|
-
3. AWS CLI profile
|
|
72
|
-
4. Default boto3 chain (env vars → ~/.aws → IMDS)
|
|
73
|
-
|
|
74
|
-
Includes automatic retry + exponential backoff.
|
|
75
|
-
Returns None if boto3 unavailable or all credential sources fail.
|
|
76
|
-
"""
|
|
77
|
-
if not HAS_BOTO3:
|
|
78
|
-
return None
|
|
79
|
-
|
|
80
|
-
if session and not access_key:
|
|
81
|
-
access_key, secret_key, session_token = _creds_from_session(session)
|
|
82
|
-
|
|
83
|
-
return _build_client(service, region, profile,
|
|
84
|
-
access_key, secret_key, session_token)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _build_client(service: str, region: str, profile: str,
|
|
88
|
-
access_key: str, secret_key: str,
|
|
89
|
-
session_token: str) -> Any:
|
|
90
|
-
"""Build boto3 client with retry config and region fallback."""
|
|
91
|
-
regions_to_try = [region] if region else FALLBACK_REGIONS
|
|
92
|
-
|
|
93
|
-
for reg in regions_to_try:
|
|
94
|
-
try:
|
|
95
|
-
if access_key and secret_key:
|
|
96
|
-
client = boto3.client(
|
|
97
|
-
service,
|
|
98
|
-
region_name=reg,
|
|
99
|
-
aws_access_key_id=access_key,
|
|
100
|
-
aws_secret_access_key=secret_key,
|
|
101
|
-
aws_session_token=session_token or None,
|
|
102
|
-
config=_RETRY_CONFIG,
|
|
103
|
-
)
|
|
104
|
-
elif profile:
|
|
105
|
-
boto_session = boto3.Session(profile_name=profile, region_name=reg)
|
|
106
|
-
client = boto_session.client(service, config=_RETRY_CONFIG)
|
|
107
|
-
else:
|
|
108
|
-
client = boto3.client(service, region_name=reg, config=_RETRY_CONFIG)
|
|
109
|
-
return client
|
|
110
|
-
except Exception:
|
|
111
|
-
continue
|
|
112
|
-
|
|
113
|
-
return None
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def get_session(session: "Optional[Session]" = None,
|
|
117
|
-
region: str = DEFAULT_REGION,
|
|
118
|
-
profile: str = "") -> Any:
|
|
119
|
-
"""Return a boto3 Session for multi-service use."""
|
|
120
|
-
if not HAS_BOTO3:
|
|
121
|
-
return None
|
|
122
|
-
|
|
123
|
-
access_key, secret_key, session_token = "", "", ""
|
|
124
|
-
if session:
|
|
125
|
-
access_key, secret_key, session_token = _creds_from_session(session)
|
|
126
|
-
|
|
127
|
-
try:
|
|
128
|
-
if access_key and secret_key:
|
|
129
|
-
return boto3.Session(
|
|
130
|
-
region_name=region,
|
|
131
|
-
aws_access_key_id=access_key,
|
|
132
|
-
aws_secret_access_key=secret_key,
|
|
133
|
-
aws_session_token=session_token or None,
|
|
134
|
-
)
|
|
135
|
-
elif profile:
|
|
136
|
-
return boto3.Session(profile_name=profile, region_name=region)
|
|
137
|
-
else:
|
|
138
|
-
return boto3.Session(region_name=region)
|
|
139
|
-
except Exception:
|
|
140
|
-
return None
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def safe_call(client: Any, method: str, **kwargs) -> tuple[bool, Any, str]:
|
|
144
|
-
"""
|
|
145
|
-
Safely call a boto3 client method with retry + error handling.
|
|
146
|
-
|
|
147
|
-
Returns: (success, response_or_None, error_message)
|
|
148
|
-
|
|
149
|
-
Usage:
|
|
150
|
-
ok, resp, err = safe_call(iam, 'list_users', MaxItems=5)
|
|
151
|
-
if ok:
|
|
152
|
-
users = resp['Users']
|
|
153
|
-
"""
|
|
154
|
-
if client is None:
|
|
155
|
-
return False, None, "boto3 client is None"
|
|
156
|
-
|
|
157
|
-
max_attempts = 3
|
|
158
|
-
for attempt in range(max_attempts):
|
|
159
|
-
try:
|
|
160
|
-
fn = getattr(client, method)
|
|
161
|
-
response = fn(**kwargs)
|
|
162
|
-
return True, response, ""
|
|
163
|
-
except botocore.exceptions.ClientError as e:
|
|
164
|
-
code = e.response["Error"]["Code"]
|
|
165
|
-
msg = e.response["Error"]["Message"]
|
|
166
|
-
# Don't retry auth errors
|
|
167
|
-
if code in ("InvalidClientTokenId", "AuthFailure",
|
|
168
|
-
"UnauthorizedAccess", "AccessDenied",
|
|
169
|
-
"ExpiredTokenException"):
|
|
170
|
-
return False, None, f"{code}: {msg}"
|
|
171
|
-
# Retry throttle errors
|
|
172
|
-
if code in ("Throttling", "RequestLimitExceeded",
|
|
173
|
-
"TooManyRequestsException"):
|
|
174
|
-
if attempt < max_attempts - 1:
|
|
175
|
-
time.sleep(2 ** attempt)
|
|
176
|
-
continue
|
|
177
|
-
return False, None, f"{code}: {msg}"
|
|
178
|
-
except Exception as e:
|
|
179
|
-
if attempt < max_attempts - 1:
|
|
180
|
-
time.sleep(1)
|
|
181
|
-
continue
|
|
182
|
-
return False, None, str(e)
|
|
183
|
-
|
|
184
|
-
return False, None, "Max retry attempts exceeded"
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def verify_credentials(access_key: str, secret_key: str,
|
|
188
|
-
session_token: str = "",
|
|
189
|
-
region: str = DEFAULT_REGION) -> dict:
|
|
190
|
-
"""
|
|
191
|
-
Verify AWS credentials via STS:GetCallerIdentity.
|
|
192
|
-
Returns structured result — never raises.
|
|
193
|
-
|
|
194
|
-
NOTE: GetCallerIdentity IS logged in CloudTrail and flagged by GuardDuty.
|
|
195
|
-
Severity: HIGH (known recon indicator).
|
|
196
|
-
"""
|
|
197
|
-
if not HAS_BOTO3:
|
|
198
|
-
return {"valid": False, "error": "boto3 not installed",
|
|
199
|
-
"guardduty_risk": "N/A"}
|
|
200
|
-
|
|
201
|
-
result = {
|
|
202
|
-
"valid": False,
|
|
203
|
-
"arn": "",
|
|
204
|
-
"account": "",
|
|
205
|
-
"user_id": "",
|
|
206
|
-
"username": "",
|
|
207
|
-
"error": "",
|
|
208
|
-
"guardduty_risk": GUARDDUTY_NOISY_APIS.get("GetCallerIdentity", "HIGH"),
|
|
209
|
-
"guardduty_note": "GetCallerIdentity is always logged and a known recon indicator",
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
client = _build_client("sts", region, "", access_key, secret_key, session_token)
|
|
213
|
-
if not client:
|
|
214
|
-
result["error"] = "Could not create STS client"
|
|
215
|
-
return result
|
|
216
|
-
|
|
217
|
-
ok, resp, err = safe_call(client, "get_caller_identity")
|
|
218
|
-
if ok:
|
|
219
|
-
result["valid"] = True
|
|
220
|
-
result["arn"] = resp.get("Arn", "")
|
|
221
|
-
result["account"] = resp.get("Account", "")
|
|
222
|
-
result["user_id"] = resp.get("UserId", "")
|
|
223
|
-
# Extract username from ARN
|
|
224
|
-
arn = result["arn"]
|
|
225
|
-
if "/" in arn:
|
|
226
|
-
result["username"] = arn.split("/")[-1]
|
|
227
|
-
else:
|
|
228
|
-
result["error"] = err
|
|
229
|
-
|
|
230
|
-
return result
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def detect_region(access_key: str, secret_key: str,
|
|
234
|
-
session_token: str = "") -> str:
|
|
235
|
-
"""
|
|
236
|
-
Detect the primary region for these credentials.
|
|
237
|
-
Falls back to us-east-1 if detection fails.
|
|
238
|
-
"""
|
|
239
|
-
if not HAS_BOTO3:
|
|
240
|
-
return DEFAULT_REGION
|
|
241
|
-
|
|
242
|
-
for region in FALLBACK_REGIONS:
|
|
243
|
-
client = _build_client("sts", region, "", access_key, secret_key, session_token)
|
|
244
|
-
ok, resp, _ = safe_call(client, "get_caller_identity")
|
|
245
|
-
if ok:
|
|
246
|
-
return region
|
|
247
|
-
|
|
248
|
-
return DEFAULT_REGION
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
def _creds_from_session(session: "Session") -> tuple[str, str, str]:
|
|
252
|
-
"""Extract best available AWS credentials from session secrets."""
|
|
253
|
-
access_key = secret_key = session_token = ""
|
|
254
|
-
for s in session.secrets:
|
|
255
|
-
t = s.get("secret_type", "")
|
|
256
|
-
v = s.get("value", "")
|
|
257
|
-
if t == "AWS_ACCESS_KEY" and not access_key:
|
|
258
|
-
access_key = v
|
|
259
|
-
elif t == "AWS_SECRET_KEY" and not secret_key:
|
|
260
|
-
secret_key = v
|
|
261
|
-
elif t == "AWS_SESSION_TOKEN" and not session_token:
|
|
262
|
-
session_token = v
|
|
263
|
-
return access_key, secret_key, session_token
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def guardduty_risk(api_name: str) -> tuple[str, str]:
|
|
267
|
-
"""
|
|
268
|
-
Return (risk_level, explanation) for a given API call.
|
|
269
|
-
Used by modules to annotate findings with detection awareness.
|
|
270
|
-
"""
|
|
271
|
-
if api_name in GUARDDUTY_NOISY_APIS:
|
|
272
|
-
return GUARDDUTY_NOISY_APIS[api_name], "GuardDuty: known high-signal event"
|
|
273
|
-
if api_name in GUARDDUTY_STEALTHY_APIS:
|
|
274
|
-
return "LOW", GUARDDUTY_STEALTHY_APIS[api_name]
|
|
275
|
-
return "LOW", "Not a known GuardDuty trigger"
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
def is_available() -> bool:
|
|
279
|
-
return HAS_BOTO3
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
def has_credentials(session: "Session") -> bool:
|
|
283
|
-
ak, sk, _ = _creds_from_session(session)
|
|
284
|
-
return bool(ak and sk)
|