maltego-transforms 1.0.0__py3-none-any.whl
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.
- maltego/_cli.py +443 -0
- maltego/_helper.py +64 -0
- maltego/auth/__init__.py +51 -0
- maltego/auth/claims.py +36 -0
- maltego/auth/dependency.py +480 -0
- maltego/auth/identity.py +110 -0
- maltego/auth/jwt_validator.py +236 -0
- maltego/auth/oidc_validator.py +118 -0
- maltego/auth/problem.py +198 -0
- maltego/auth/saml_validator.py +557 -0
- maltego/auth/settings.py +733 -0
- maltego/auth/validator.py +125 -0
- maltego/config.py +102 -0
- maltego/middlewares/middlewares.py +79 -0
- maltego/middlewares/oauth_middleware.py +213 -0
- maltego/middlewares/shared_settings_middleware.py +81 -0
- maltego/middlewares/user_concurrency_limit_middleware.py +113 -0
- maltego/middlewares/verify_metadata_middleware.py +72 -0
- maltego/model/__init__.py +113 -0
- maltego/model/context.py +678 -0
- maltego/model/entity/__init__.py +1675 -0
- maltego/model/entity/config.py +231 -0
- maltego/model/entity/constants.py +86 -0
- maltego/model/entity/display_info.py +26 -0
- maltego/model/entity/property.py +369 -0
- maltego/model/entity/type_handling.py +36 -0
- maltego/model/event.py +329 -0
- maltego/model/exception.py +182 -0
- maltego/model/graph.py +656 -0
- maltego/model/icon.py +9 -0
- maltego/model/input_constraints/__init__.py +35 -0
- maltego/model/input_constraints/base.py +288 -0
- maltego/model/input_constraints/entity/__init__.py +13 -0
- maltego/model/input_constraints/entity/composite.py +60 -0
- maltego/model/input_constraints/entity/property.py +81 -0
- maltego/model/input_constraints/entity/type.py +42 -0
- maltego/model/input_constraints/property/__init__.py +25 -0
- maltego/model/input_constraints/property/composite.py +64 -0
- maltego/model/input_constraints/property/equals.py +336 -0
- maltego/model/input_constraints/property/match.py +330 -0
- maltego/model/input_constraints/property/regex.py +221 -0
- maltego/model/link.py +369 -0
- maltego/model/machine.py +59 -0
- maltego/model/oauth.py +155 -0
- maltego/model/observer.py +53 -0
- maltego/model/prompt.py +305 -0
- maltego/model/server.py +952 -0
- maltego/model/transform/__init__.py +955 -0
- maltego/model/transform/setting.py +420 -0
- maltego/model/transform_input_annotation.py +322 -0
- maltego/model/transform_set.py +8 -0
- maltego/model/types.py +342 -0
- maltego/pagination/__init__.py +10 -0
- maltego/pagination/cursor_based_paginator.py +55 -0
- maltego/pagination/offset_limit_paginator.py +119 -0
- maltego/pagination/page_based_paginator.py +95 -0
- maltego/pagination/pagination.py +677 -0
- maltego/protocol/__init__.py +0 -0
- maltego/protocol/v3/__init__.py +0 -0
- maltego/protocol/v3/discovery/__init__.py +16 -0
- maltego/protocol/v3/discovery/auth.py +23 -0
- maltego/protocol/v3/discovery/capability.py +16 -0
- maltego/protocol/v3/discovery/entity.py +71 -0
- maltego/protocol/v3/discovery/hub_item.py +21 -0
- maltego/protocol/v3/discovery/icon.py +19 -0
- maltego/protocol/v3/discovery/machine.py +26 -0
- maltego/protocol/v3/discovery/seed.py +19 -0
- maltego/protocol/v3/discovery/status.py +15 -0
- maltego/protocol/v3/discovery/transform.py +71 -0
- maltego/protocol/v3/discovery/transform_set.py +12 -0
- maltego/protocol/v3/error.py +13 -0
- maltego/protocol/v3/execution/__init__.py +0 -0
- maltego/protocol/v3/execution/entity.py +46 -0
- maltego/protocol/v3/execution/graph.py +13 -0
- maltego/protocol/v3/execution/link.py +17 -0
- maltego/protocol/v3/execution/metadata.py +12 -0
- maltego/protocol/v3/execution/property.py +21 -0
- maltego/protocol/v3/execution/transform_run.py +208 -0
- maltego/protocol/v3/execution/ui_message.py +25 -0
- maltego/py.typed +0 -0
- maltego/runner/__init__.py +477 -0
- maltego/runner/transform_execution_context.py +620 -0
- maltego/runner/transform_result_set.py +261 -0
- maltego/server/__init__.py +1578 -0
- maltego/server/capability_matrix.py +432 -0
- maltego/server/etag_middleware.py +22 -0
- maltego/server/tracing_middleware.py +71 -0
- maltego/server/util.py +96 -0
- maltego/server/v3/__init__.py +1611 -0
- maltego/server/version.py +2 -0
- maltego/skills_assets/README.md +1 -0
- maltego/skills_assets/maltego-transform-basics/SKILL.md +27 -0
- maltego/skills_assets/maltego-transform-basics/references/transform-authoring-patterns.md +257 -0
- maltego/skills_assets/maltego-transform-build/SKILL.md +173 -0
- maltego/skills_assets/maltego-transform-design/SKILL.md +80 -0
- maltego/skills_assets/maltego-transform-design/references/standard-entity-selection.md +148 -0
- maltego/skills_assets/maltego-transform-discover/SKILL.md +91 -0
- maltego/skills_assets/maltego-transform-discover/references/direct-server-discovery.md +189 -0
- maltego/skills_assets/maltego-transform-discover/scripts/discover_server.py +97 -0
- maltego/skills_assets/maltego-transform-docs/SKILL.md +54 -0
- maltego/skills_assets/maltego-transform-docs/references/docs-routing.md +58 -0
- maltego/skills_assets/maltego-transform-skill-index/SKILL.md +60 -0
- maltego/skills_assets/maltego-transform-test/SKILL.md +214 -0
- maltego/skills_assets/maltego-transform-test/references/testing-and-parity.md +300 -0
- maltego/skills_assets/maltego-transform-test/scripts/README.md +29 -0
- maltego/skills_assets/maltego-transform-test/scripts/sdk_project_check.py +283 -0
- maltego/skills_assets/maltego-transforms/SKILL.md +53 -0
- maltego/skills_assets/maltego-trx-migration-implementer/SKILL.md +244 -0
- maltego/skills_assets/maltego-trx-migration-implementer/references/trx-to-sdk-mapping.md +177 -0
- maltego/skills_assets/maltego-trx-migration-implementer/scripts/README.md +42 -0
- maltego/skills_assets/maltego-trx-migration-implementer/scripts/trx_to_sdk_candidates.py +223 -0
- maltego/skills_assets/maltego-trx-migration-planner/SKILL.md +230 -0
- maltego/skills_assets/maltego-trx-migration-planner/references/known-trx-examples.md +96 -0
- maltego/skills_assets/maltego-trx-migration-planner/references/standard-entity-selection.md +148 -0
- maltego/skills_assets/maltego-trx-migration-planner/references/trx-risk-taxonomy.md +142 -0
- maltego/skills_assets/maltego-trx-migration-planner/references/trx-to-sdk-mapping.md +177 -0
- maltego/skills_assets/maltego-trx-migration-planner/scripts/README.md +76 -0
- maltego/skills_assets/maltego-trx-migration-planner/scripts/std_entity_lookup.py +142 -0
- maltego/skills_assets/maltego-trx-migration-planner/scripts/trx_contract.py +312 -0
- maltego/skills_assets/maltego-trx-migration-planner/scripts/trx_inventory.py +297 -0
- maltego/skills_assets/maltego-trx-migration-planner/scripts/trx_migration_report.py +222 -0
- maltego/template_dir/README.md +105 -0
- maltego/template_dir/__init__.py +0 -0
- maltego/template_dir/project.py +34 -0
- maltego/template_dir/requirements.txt +2 -0
- maltego/template_dir/transforms/__init__.py +0 -0
- maltego/template_dir/transforms/entity_features_example.py +217 -0
- maltego/template_dir/transforms/error_handling_example.py +206 -0
- maltego/template_dir/transforms/input_constraints_example.py +262 -0
- maltego/template_dir/transforms/logging_example.py +120 -0
- maltego/template_dir/transforms/middleware_example.py +197 -0
- maltego/template_dir/transforms/pagination_example.py +325 -0
- maltego/template_dir/transforms/prompts_example.py +373 -0
- maltego/template_dir/transforms/quickstart_example.py +349 -0
- maltego/template_dir/transforms/transform_settings_example.py +481 -0
- maltego/template_minimal_dir/README.md +84 -0
- maltego/template_minimal_dir/__init__.py +1 -0
- maltego/template_minimal_dir/project.py +21 -0
- maltego/template_minimal_dir/requirements.txt +2 -0
- maltego/template_minimal_dir/transforms/__init__.py +1 -0
- maltego/tracing/__init__.py +133 -0
- maltego/util/__init__.py +568 -0
- maltego/util/collections_utils.py +8 -0
- maltego/util/trace_context.py +3 -0
- maltego/util/typing.py +23 -0
- maltego_transforms-1.0.0.dist-info/LICENSE +21 -0
- maltego_transforms-1.0.0.dist-info/METADATA +189 -0
- maltego_transforms-1.0.0.dist-info/RECORD +150 -0
- maltego_transforms-1.0.0.dist-info/WHEEL +4 -0
- maltego_transforms-1.0.0.dist-info/entry_points.txt +3 -0
maltego/_cli.py
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import shutil
|
|
4
|
+
import sys
|
|
5
|
+
from typing import List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
import maltego
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
Receive commands run to start a new project.
|
|
11
|
+
"""
|
|
12
|
+
FORCE_OVERWRITE = False
|
|
13
|
+
DEMO_TEMPLATE = "template_dir"
|
|
14
|
+
MINIMAL_TEMPLATE = "template_minimal_dir"
|
|
15
|
+
DEFAULT_PROJECT_MANAGER = "bare"
|
|
16
|
+
PROJECT_MANAGERS = {"bare", "poetry", "uv"}
|
|
17
|
+
START_USAGE = """Usage:
|
|
18
|
+
maltego-transforms start [options] <project_name>
|
|
19
|
+
|
|
20
|
+
Create a new Maltego Transform SDK project.
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--minimal Generate the compact starter template.
|
|
24
|
+
--demo Generate the example-rich starter template (default).
|
|
25
|
+
--project-manager bare|poetry|uv Add project metadata for the selected tool.
|
|
26
|
+
--with-skills Add local SDK agent skills under .agents/skills/.
|
|
27
|
+
--skills-scope local|global Install skills into the project (local) or your global
|
|
28
|
+
agent skills directory. Implies --with-skills.
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
maltego-transforms start my_transforms
|
|
32
|
+
maltego-transforms start --minimal --with-skills --project-manager uv my_transforms
|
|
33
|
+
|
|
34
|
+
Add SDK skills to an existing project:
|
|
35
|
+
maltego-transforms install-skills --target .
|
|
36
|
+
"""
|
|
37
|
+
INIT_USAGE = """Usage:
|
|
38
|
+
maltego-transforms init [options]
|
|
39
|
+
|
|
40
|
+
Initialize the current directory as a Maltego Transform SDK project.
|
|
41
|
+
|
|
42
|
+
Options:
|
|
43
|
+
--minimal Generate the compact starter template.
|
|
44
|
+
--demo Generate the example-rich starter template (default).
|
|
45
|
+
--project-manager bare|poetry|uv Add project metadata for the selected tool.
|
|
46
|
+
--with-skills Add local SDK agent skills under .agents/skills/.
|
|
47
|
+
--skills-scope local|global Install skills into the project (local) or your global
|
|
48
|
+
agent skills directory. Implies --with-skills.
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
maltego-transforms init --minimal --with-skills
|
|
52
|
+
maltego-transforms init --project-manager poetry
|
|
53
|
+
"""
|
|
54
|
+
INSTALL_SKILLS_USAGE = """Usage:
|
|
55
|
+
maltego-transforms install-skills [--scope local|global] [--target DIR]
|
|
56
|
+
|
|
57
|
+
Install Maltego SDK agent skills without generating or changing starter files.
|
|
58
|
+
|
|
59
|
+
Options:
|
|
60
|
+
--target DIR Existing project directory to receive local skills.
|
|
61
|
+
Defaults to the current directory.
|
|
62
|
+
--scope local|global local installs into DIR/.agents/skills and adds
|
|
63
|
+
project bootstrap notes. global installs into the
|
|
64
|
+
configured global agent skills directory.
|
|
65
|
+
|
|
66
|
+
Examples:
|
|
67
|
+
maltego-transforms install-skills --target .
|
|
68
|
+
maltego-transforms install-skills --scope global
|
|
69
|
+
"""
|
|
70
|
+
COMMAND_USAGE = f"""Usage:
|
|
71
|
+
maltego-transforms <command> [options]
|
|
72
|
+
|
|
73
|
+
Commands:
|
|
74
|
+
start Create a new SDK project.
|
|
75
|
+
init Initialize the current directory as an SDK project.
|
|
76
|
+
install-skills Add SDK agent skills to an existing project or global skills directory.
|
|
77
|
+
|
|
78
|
+
Run `maltego-transforms <command> --help` for command-specific options.
|
|
79
|
+
"""
|
|
80
|
+
LOCAL_SKILLS_BOOTSTRAP_MARKER = "<!-- maltego-transforms-skills -->"
|
|
81
|
+
LOCAL_SKILLS_BOOTSTRAP = f"""{LOCAL_SKILLS_BOOTSTRAP_MARKER}
|
|
82
|
+
|
|
83
|
+
## Maltego Transform SDK Skills
|
|
84
|
+
|
|
85
|
+
This project includes local Maltego SDK skills under `.agents/skills`.
|
|
86
|
+
When working on transforms in this project, first read:
|
|
87
|
+
|
|
88
|
+
`.agents/skills/maltego-transform-skill-index/SKILL.md`
|
|
89
|
+
|
|
90
|
+
That index routes agents to focused skills for SDK v3 authoring, TRX migration,
|
|
91
|
+
server discovery, docs lookup, and testing.
|
|
92
|
+
"""
|
|
93
|
+
LOCAL_SKILLS_README = """# Maltego Transform SDK Skills
|
|
94
|
+
|
|
95
|
+
This project includes local skills in `.agents/skills`.
|
|
96
|
+
|
|
97
|
+
Agents that support project-local skills can load them when started from this
|
|
98
|
+
project directory. If skills are not auto-discovered, read:
|
|
99
|
+
|
|
100
|
+
`.agents/skills/maltego-transform-skill-index/SKILL.md`
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def execute_from_command_line() -> None:
|
|
105
|
+
args = sys.argv[1:]
|
|
106
|
+
try:
|
|
107
|
+
args, with_skills, skills_scope = parse_skills_options(args)
|
|
108
|
+
except ValueError as e:
|
|
109
|
+
print(f"ERROR: {e}")
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
if not args:
|
|
113
|
+
print(COMMAND_USAGE)
|
|
114
|
+
elif args[0].lower() == "start":
|
|
115
|
+
run_start(args[1:], with_skills=with_skills, skills_scope=skills_scope)
|
|
116
|
+
elif args[0].lower() == "init":
|
|
117
|
+
run_start(args[1:], create_dir=False, with_skills=with_skills,
|
|
118
|
+
skills_scope=skills_scope)
|
|
119
|
+
elif args[0].lower() == "install-skills":
|
|
120
|
+
run_install_skills(args[1:])
|
|
121
|
+
else:
|
|
122
|
+
print(COMMAND_USAGE)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def parse_skills_options(args: List[str]) -> Tuple[List[str], bool, str]:
|
|
126
|
+
"""Extract skills-related CLI options from argv-style input."""
|
|
127
|
+
cleaned_args: List[str] = []
|
|
128
|
+
with_skills = False
|
|
129
|
+
skills_scope = "local"
|
|
130
|
+
i = 0
|
|
131
|
+
while i < len(args):
|
|
132
|
+
arg = args[i]
|
|
133
|
+
if arg == "--with-skills":
|
|
134
|
+
with_skills = True
|
|
135
|
+
elif arg == "--skills-scope":
|
|
136
|
+
if i + 1 >= len(args):
|
|
137
|
+
raise ValueError("--skills-scope requires 'local' or 'global'")
|
|
138
|
+
skills_scope = args[i + 1].lower()
|
|
139
|
+
with_skills = True
|
|
140
|
+
i += 1
|
|
141
|
+
elif arg.startswith("--skills-scope="):
|
|
142
|
+
skills_scope = arg.split("=", 1)[1].lower()
|
|
143
|
+
with_skills = True
|
|
144
|
+
else:
|
|
145
|
+
cleaned_args.append(arg)
|
|
146
|
+
i += 1
|
|
147
|
+
|
|
148
|
+
if skills_scope not in {"local", "global"}:
|
|
149
|
+
raise ValueError("--skills-scope must be 'local' or 'global'")
|
|
150
|
+
return cleaned_args, with_skills, skills_scope
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def find_template_dir(template_name: str = DEMO_TEMPLATE) -> str:
|
|
154
|
+
"""Find a project template directory in the maltego namespace paths."""
|
|
155
|
+
for path in maltego.__path__:
|
|
156
|
+
template_path = os.path.join(path, template_name)
|
|
157
|
+
if os.path.isdir(template_path):
|
|
158
|
+
return template_path
|
|
159
|
+
raise FileNotFoundError(f"Could not find {template_name} in maltego package paths")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def parse_start_args(args: List[str], create_dir: bool) -> Optional[Tuple[Optional[str], str, str]]:
|
|
163
|
+
template_name = DEMO_TEMPLATE
|
|
164
|
+
project_manager = DEFAULT_PROJECT_MANAGER
|
|
165
|
+
project_args = []
|
|
166
|
+
index = 0
|
|
167
|
+
|
|
168
|
+
while index < len(args):
|
|
169
|
+
arg = args[index]
|
|
170
|
+
if arg in {"-h", "--help"}:
|
|
171
|
+
print(START_USAGE if create_dir else INIT_USAGE)
|
|
172
|
+
return None
|
|
173
|
+
if arg == "--minimal":
|
|
174
|
+
template_name = MINIMAL_TEMPLATE
|
|
175
|
+
elif arg == "--demo":
|
|
176
|
+
template_name = DEMO_TEMPLATE
|
|
177
|
+
elif arg == "--project-manager":
|
|
178
|
+
index += 1
|
|
179
|
+
if index >= len(args):
|
|
180
|
+
print(START_USAGE if create_dir else INIT_USAGE)
|
|
181
|
+
sys.exit(2)
|
|
182
|
+
project_manager = args[index].lower()
|
|
183
|
+
if project_manager not in PROJECT_MANAGERS:
|
|
184
|
+
print(START_USAGE if create_dir else INIT_USAGE)
|
|
185
|
+
sys.exit(2)
|
|
186
|
+
else:
|
|
187
|
+
project_args.append(arg)
|
|
188
|
+
index += 1
|
|
189
|
+
|
|
190
|
+
if create_dir:
|
|
191
|
+
if len(project_args) != 1:
|
|
192
|
+
print(START_USAGE)
|
|
193
|
+
return None
|
|
194
|
+
if not is_safe_project_directory_name(project_args[0]):
|
|
195
|
+
print("Project name must be a simple directory name without path separators.")
|
|
196
|
+
return None
|
|
197
|
+
if project_manager != DEFAULT_PROJECT_MANAGER and not normalize_project_name(project_args[0]):
|
|
198
|
+
print(
|
|
199
|
+
"Project name must contain at least one alphanumeric character "
|
|
200
|
+
"for a valid pyproject name."
|
|
201
|
+
)
|
|
202
|
+
sys.exit(2)
|
|
203
|
+
return project_args[0], template_name, project_manager
|
|
204
|
+
|
|
205
|
+
if project_args:
|
|
206
|
+
print(INIT_USAGE)
|
|
207
|
+
return None
|
|
208
|
+
return None, template_name, project_manager
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def normalize_project_name(project_name: str) -> str:
|
|
212
|
+
normalized_name = re.sub(r"[^a-z0-9]+", "-", project_name.lower())
|
|
213
|
+
return normalized_name.strip("-")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def is_safe_project_directory_name(project_name: str) -> bool:
|
|
217
|
+
return (
|
|
218
|
+
bool(project_name)
|
|
219
|
+
and project_name not in {".", ".."}
|
|
220
|
+
and not project_name.startswith("-")
|
|
221
|
+
and not os.path.isabs(project_name)
|
|
222
|
+
and "/" not in project_name
|
|
223
|
+
and "\\" not in project_name
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def render_pyproject(project_name: str, project_manager: str) -> Optional[str]:
|
|
228
|
+
if project_manager == DEFAULT_PROJECT_MANAGER:
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
normalized_name = normalize_project_name(project_name)
|
|
232
|
+
pyproject_lines = [
|
|
233
|
+
"[project]",
|
|
234
|
+
f'name = "{normalized_name}"',
|
|
235
|
+
'version = "0.1.0"',
|
|
236
|
+
'requires-python = ">=3.10"',
|
|
237
|
+
"dependencies = [",
|
|
238
|
+
' "maltego-transforms>=1.0.0",',
|
|
239
|
+
' "maltego-transforms-std-entities>=1.0.0",',
|
|
240
|
+
"]",
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
if project_manager == "poetry":
|
|
244
|
+
pyproject_lines.extend(["", "[tool.poetry]", "package-mode = false"])
|
|
245
|
+
|
|
246
|
+
return "\n".join(pyproject_lines) + "\n"
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def apply_project_manager(project_dir: str, project_name: str, project_manager: str) -> None:
|
|
250
|
+
pyproject_text = render_pyproject(project_name, project_manager)
|
|
251
|
+
if pyproject_text is None:
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
pyproject_path = os.path.join(project_dir, "pyproject.toml")
|
|
255
|
+
with open(pyproject_path, "w", encoding="utf-8") as pyproject_file:
|
|
256
|
+
pyproject_file.write(pyproject_text)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def find_skills_dir() -> str:
|
|
260
|
+
"""Find skills_assets in the maltego namespace paths."""
|
|
261
|
+
for path in maltego.__path__:
|
|
262
|
+
skills_path = os.path.join(path, "skills_assets")
|
|
263
|
+
if os.path.isdir(skills_path):
|
|
264
|
+
return skills_path
|
|
265
|
+
raise FileNotFoundError("Could not find skills_assets in maltego package paths")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def find_global_skills_dir(skills_home: str | None = None) -> str:
|
|
269
|
+
"""Return the provider-agnostic global skills directory."""
|
|
270
|
+
if skills_home:
|
|
271
|
+
return skills_home
|
|
272
|
+
return os.path.join(os.path.expanduser("~"), ".agents", "skills")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def write_local_skills_bootstrap(project_dir: str) -> None:
|
|
276
|
+
"""Create local pointers that tell agents how to load generated skills."""
|
|
277
|
+
agents_dir = os.path.join(project_dir, ".agents")
|
|
278
|
+
os.makedirs(agents_dir, exist_ok=True)
|
|
279
|
+
|
|
280
|
+
readme_path = os.path.join(agents_dir, "README.md")
|
|
281
|
+
with open(readme_path, "w", encoding="utf-8") as f:
|
|
282
|
+
f.write(LOCAL_SKILLS_README)
|
|
283
|
+
|
|
284
|
+
agents_md_path = os.path.join(project_dir, "AGENTS.md")
|
|
285
|
+
if os.path.exists(agents_md_path):
|
|
286
|
+
with open(agents_md_path, encoding="utf-8") as f:
|
|
287
|
+
existing = f.read()
|
|
288
|
+
if LOCAL_SKILLS_BOOTSTRAP_MARKER in existing:
|
|
289
|
+
return
|
|
290
|
+
separator = "" if existing.endswith("\n") else "\n"
|
|
291
|
+
with open(agents_md_path, "a", encoding="utf-8") as f:
|
|
292
|
+
f.write(f"{separator}\n{LOCAL_SKILLS_BOOTSTRAP}")
|
|
293
|
+
else:
|
|
294
|
+
with open(agents_md_path, "w", encoding="utf-8") as f:
|
|
295
|
+
f.write(f"# Agent Guide\n\n{LOCAL_SKILLS_BOOTSTRAP}")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def install_skills(project_dir: str, skills_scope: str = "local", skills_home: str | None = None) -> str:
|
|
299
|
+
"""Install SDK agent skills into an existing project or global skills directory."""
|
|
300
|
+
if skills_scope not in {"local", "global"}:
|
|
301
|
+
raise ValueError("skills_scope must be 'local' or 'global'")
|
|
302
|
+
|
|
303
|
+
skills_dir_path = find_skills_dir()
|
|
304
|
+
if skills_scope == "global":
|
|
305
|
+
skills_dst = find_global_skills_dir(skills_home)
|
|
306
|
+
else:
|
|
307
|
+
skills_dst = os.path.join(project_dir, ".agents", "skills")
|
|
308
|
+
os.makedirs(skills_dst, exist_ok=True)
|
|
309
|
+
copytree(src=skills_dir_path, dst=skills_dst, dirs_exist_ok=True)
|
|
310
|
+
if skills_scope == "local":
|
|
311
|
+
write_local_skills_bootstrap(project_dir)
|
|
312
|
+
return skills_dst
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def parse_install_skills_args(args: List[str]) -> Tuple[str, str]:
|
|
316
|
+
"""Parse install-skills arguments into (scope, target directory)."""
|
|
317
|
+
skills_scope = "local"
|
|
318
|
+
target_dir = os.getcwd()
|
|
319
|
+
index = 0
|
|
320
|
+
while index < len(args):
|
|
321
|
+
arg = args[index]
|
|
322
|
+
if arg in {"-h", "--help"}:
|
|
323
|
+
print(INSTALL_SKILLS_USAGE)
|
|
324
|
+
sys.exit(0)
|
|
325
|
+
if arg == "--scope":
|
|
326
|
+
index += 1
|
|
327
|
+
if index >= len(args):
|
|
328
|
+
print(INSTALL_SKILLS_USAGE)
|
|
329
|
+
sys.exit(2)
|
|
330
|
+
skills_scope = args[index].lower()
|
|
331
|
+
elif arg.startswith("--scope="):
|
|
332
|
+
skills_scope = arg.split("=", 1)[1].lower()
|
|
333
|
+
elif arg == "--target":
|
|
334
|
+
index += 1
|
|
335
|
+
if index >= len(args):
|
|
336
|
+
print(INSTALL_SKILLS_USAGE)
|
|
337
|
+
sys.exit(2)
|
|
338
|
+
target_dir = args[index]
|
|
339
|
+
elif arg.startswith("--target="):
|
|
340
|
+
target_dir = arg.split("=", 1)[1]
|
|
341
|
+
else:
|
|
342
|
+
print(INSTALL_SKILLS_USAGE)
|
|
343
|
+
sys.exit(2)
|
|
344
|
+
index += 1
|
|
345
|
+
|
|
346
|
+
if skills_scope not in {"local", "global"}:
|
|
347
|
+
print(INSTALL_SKILLS_USAGE)
|
|
348
|
+
sys.exit(2)
|
|
349
|
+
return skills_scope, target_dir
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def run_install_skills(args: List[str], skills_home: str | None = None) -> None:
|
|
353
|
+
"""Install SDK skills without generating a starter project."""
|
|
354
|
+
skills_scope, target_dir = parse_install_skills_args(args)
|
|
355
|
+
project_dir = os.path.abspath(target_dir)
|
|
356
|
+
if skills_scope == "local" and not os.path.isdir(project_dir):
|
|
357
|
+
print(f"ERROR: target directory does not exist: {target_dir}")
|
|
358
|
+
sys.exit(2)
|
|
359
|
+
|
|
360
|
+
skills_dst = install_skills(project_dir, skills_scope, skills_home)
|
|
361
|
+
if skills_scope == "global":
|
|
362
|
+
print(f"Successfully installed SDK skills to '{skills_dst}'.")
|
|
363
|
+
print("Agents that use this global skills directory can load the Maltego SDK skills from any project.")
|
|
364
|
+
else:
|
|
365
|
+
print(f"Successfully added SDK skills to '{skills_dst}'.")
|
|
366
|
+
print("Agents that start in this project directory can load them from '.agents/skills/'.")
|
|
367
|
+
print("If skills are not auto-discovered, read '.agents/skills/maltego-transform-skill-index/SKILL.md' first.")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def run_start(
|
|
371
|
+
args: List[str],
|
|
372
|
+
create_dir: bool = True,
|
|
373
|
+
with_skills: bool = False,
|
|
374
|
+
skills_scope: str = "local",
|
|
375
|
+
skills_home: str | None = None,
|
|
376
|
+
) -> None:
|
|
377
|
+
if skills_scope not in {"local", "global"}:
|
|
378
|
+
raise ValueError("skills_scope must be 'local' or 'global'")
|
|
379
|
+
|
|
380
|
+
parsed_args = parse_start_args(args, create_dir)
|
|
381
|
+
if parsed_args is None:
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
project, template_name, project_manager = parsed_args
|
|
385
|
+
if create_dir:
|
|
386
|
+
assert project is not None
|
|
387
|
+
project_dir = os.path.join(os.getcwd(), project)
|
|
388
|
+
# Confine the generated project to the current working directory; reject
|
|
389
|
+
# names that escape it (e.g. absolute paths or "../").
|
|
390
|
+
cwd = os.path.abspath(os.getcwd())
|
|
391
|
+
if os.path.commonpath([cwd, os.path.abspath(project_dir)]) != cwd:
|
|
392
|
+
print(f"ERROR: project name must stay within the current directory: {project!r}")
|
|
393
|
+
sys.exit(1)
|
|
394
|
+
try:
|
|
395
|
+
os.makedirs(project_dir)
|
|
396
|
+
except FileExistsError:
|
|
397
|
+
if not FORCE_OVERWRITE:
|
|
398
|
+
print(f"Project directory {project_dir} already exists. Overwrite? [y/n]", end=" ")
|
|
399
|
+
choice = input().lower()
|
|
400
|
+
if choice in ['yes', 'y']:
|
|
401
|
+
pass
|
|
402
|
+
else:
|
|
403
|
+
sys.exit(1)
|
|
404
|
+
else:
|
|
405
|
+
project_dir = os.getcwd()
|
|
406
|
+
project = os.path.basename(project_dir)
|
|
407
|
+
if project_manager != DEFAULT_PROJECT_MANAGER and not normalize_project_name(project):
|
|
408
|
+
print(
|
|
409
|
+
"Project name must contain at least one alphanumeric character "
|
|
410
|
+
"for a valid pyproject name."
|
|
411
|
+
)
|
|
412
|
+
sys.exit(2)
|
|
413
|
+
try:
|
|
414
|
+
template_dir_path = find_template_dir(template_name)
|
|
415
|
+
copytree(src=template_dir_path, dst=project_dir, dirs_exist_ok=True)
|
|
416
|
+
apply_project_manager(project_dir, project, project_manager)
|
|
417
|
+
print(f"Successfully created a new project in the '{project}' folder.")
|
|
418
|
+
|
|
419
|
+
if with_skills:
|
|
420
|
+
skills_dst = install_skills(project_dir, skills_scope, skills_home)
|
|
421
|
+
if skills_scope == "global":
|
|
422
|
+
print(f"Successfully installed SDK skills to '{skills_dst}'.")
|
|
423
|
+
print("Agents that use this global skills directory can load the Maltego SDK skills from any project.")
|
|
424
|
+
else:
|
|
425
|
+
print("Successfully added SDK skills to '.agents/skills/'.")
|
|
426
|
+
print("Agents that start in this project directory can load them from '.agents/skills/'.")
|
|
427
|
+
print("If skills are not auto-discovered, read '.agents/skills/maltego-transform-skill-index/SKILL.md' first.")
|
|
428
|
+
except FileExistsError:
|
|
429
|
+
print(f"ERROR: '{project_dir}' already exists")
|
|
430
|
+
except OSError as e:
|
|
431
|
+
print(f"ERROR: {e}")
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def copytree(src: str, dst: str, symlinks: bool = False, dirs_exist_ok: bool = False) -> None:
|
|
435
|
+
os.makedirs(dst, exist_ok=dirs_exist_ok)
|
|
436
|
+
for item in os.listdir(src):
|
|
437
|
+
if "__pycache__" not in item and ".pyc" not in item and item != ".gitkeep":
|
|
438
|
+
source_path = os.path.join(src, item)
|
|
439
|
+
dst_path = os.path.join(dst, item)
|
|
440
|
+
if os.path.isdir(source_path):
|
|
441
|
+
copytree(src=source_path, dst=dst_path, symlinks=symlinks, dirs_exist_ok=dirs_exist_ok)
|
|
442
|
+
else:
|
|
443
|
+
shutil.copy2(source_path, dst_path)
|
maltego/_helper.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from typing import Any, Optional, Tuple, List
|
|
2
|
+
import uuid
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
MAJOR_VERSION_REGEX_GROUP_NAME = "major"
|
|
7
|
+
MINOR_VERSION_REGEX_GROUP_NAME = "minor"
|
|
8
|
+
PATCH_VERSION_REGEX_GROUP_NAME = "patch"
|
|
9
|
+
PRERELEASE_VERSION_REGEX_GROUP_NAME = "prerelease"
|
|
10
|
+
BUILD_META_VERSION_REGEX_GROUP_NAME = "buildmetadata"
|
|
11
|
+
EXTRA_REGEX_GROUP_NAME = "extra"
|
|
12
|
+
USER_AGENT_PATTERN = re.compile(
|
|
13
|
+
r"^Maltego Desktop\/(?P<" +
|
|
14
|
+
MAJOR_VERSION_REGEX_GROUP_NAME + r">0|[1-9]\d*)\.(?P<" +
|
|
15
|
+
MINOR_VERSION_REGEX_GROUP_NAME + r">0|[1-9]\d*)\.(?P<" +
|
|
16
|
+
PATCH_VERSION_REGEX_GROUP_NAME + r">0|[1-9]\d*)(?:-(?P<" +
|
|
17
|
+
PRERELEASE_VERSION_REGEX_GROUP_NAME + r">(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<" + # pylint: disable=line-too-long
|
|
18
|
+
BUILD_META_VERSION_REGEX_GROUP_NAME + r">[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))? \((?P<" +
|
|
19
|
+
EXTRA_REGEX_GROUP_NAME + r">[^)]+)\)?$"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_maltego_id() -> str:
|
|
24
|
+
# Generate a new UUID
|
|
25
|
+
guid: uuid.UUID = uuid.uuid4()
|
|
26
|
+
# Convert the UUID to an integer and mask the lower 64 bits
|
|
27
|
+
guid_int: int = int(guid.int) & ((1 << 64) - 1)
|
|
28
|
+
# Define the base36 alphabet
|
|
29
|
+
alphabet: str = "0123456789abcdefghijklmnopqrstuvwxyz"
|
|
30
|
+
# Convert the integer to a base36 string
|
|
31
|
+
base36: str = ''
|
|
32
|
+
while guid_int != 0:
|
|
33
|
+
guid_int, i = divmod(guid_int, len(alphabet))
|
|
34
|
+
base36 = alphabet[i] + base36
|
|
35
|
+
# Return the base36 string
|
|
36
|
+
|
|
37
|
+
return base36
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def __parse_group(matches: re.Match[str], groupname: str, target_type: type) -> Any:
|
|
41
|
+
try:
|
|
42
|
+
return target_type(matches.group(groupname))
|
|
43
|
+
except ValueError:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parse_ua(
|
|
48
|
+
user_agent_string: Optional[str]
|
|
49
|
+
) -> Tuple[Optional[int], Optional[int], Optional[int], Optional[str], Optional[str], Optional[List[str]]]:
|
|
50
|
+
matches = USER_AGENT_PATTERN.fullmatch(user_agent_string) if user_agent_string else None
|
|
51
|
+
if matches is not None:
|
|
52
|
+
major_version = __parse_group(matches, MAJOR_VERSION_REGEX_GROUP_NAME, int)
|
|
53
|
+
minor_version = __parse_group(matches, MINOR_VERSION_REGEX_GROUP_NAME, int)
|
|
54
|
+
patch_version = __parse_group(matches, PATCH_VERSION_REGEX_GROUP_NAME, int)
|
|
55
|
+
prerelease = __parse_group(matches, PRERELEASE_VERSION_REGEX_GROUP_NAME, str)
|
|
56
|
+
build_metadata = __parse_group(matches, BUILD_META_VERSION_REGEX_GROUP_NAME, str)
|
|
57
|
+
extra_str = __parse_group(matches, EXTRA_REGEX_GROUP_NAME, str)
|
|
58
|
+
extra = None
|
|
59
|
+
if isinstance(extra_str, str):
|
|
60
|
+
extra = [extra.strip() for extra in extra_str.split(";")]
|
|
61
|
+
else:
|
|
62
|
+
extra = None
|
|
63
|
+
return (major_version, minor_version, patch_version, prerelease, build_metadata, extra)
|
|
64
|
+
return (None, None, None, None, None, None)
|
maltego/auth/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Copyright (c) Maltego Technologies GmbH.
|
|
2
|
+
"""
|
|
3
|
+
Maltego authentication module.
|
|
4
|
+
|
|
5
|
+
Provides optional JWT/OIDC authentication for Maltego transform servers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from maltego.auth.dependency import close_validator, optional_auth, optional_bearer
|
|
9
|
+
from maltego.auth.identity import AuthContext, Identity
|
|
10
|
+
from maltego.auth.jwt_validator import JWTTokenValidator
|
|
11
|
+
from maltego.auth.oidc_validator import OIDCTokenValidator
|
|
12
|
+
from maltego.auth.saml_validator import SAMLTokenValidator
|
|
13
|
+
from maltego.auth.settings import (
|
|
14
|
+
AuthMode,
|
|
15
|
+
AuthProviderType,
|
|
16
|
+
AuthTokenOrigin,
|
|
17
|
+
AuthSettings,
|
|
18
|
+
get_auth_settings,
|
|
19
|
+
set_auth_settings,
|
|
20
|
+
reset_auth_settings,
|
|
21
|
+
)
|
|
22
|
+
from maltego.auth.validator import AuthValidationFailure, AuthValidationSuccess, TokenValidator, ValidationErrorKind
|
|
23
|
+
from maltego.auth.problem import MaltegoAuthErrorCode, MaltegoAuthProblemDetail, AuthProblemException
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
# Dependencies
|
|
27
|
+
"optional_auth",
|
|
28
|
+
"optional_bearer",
|
|
29
|
+
"close_validator",
|
|
30
|
+
# Settings
|
|
31
|
+
"AuthMode",
|
|
32
|
+
"AuthProviderType",
|
|
33
|
+
"AuthTokenOrigin",
|
|
34
|
+
"AuthSettings",
|
|
35
|
+
"get_auth_settings",
|
|
36
|
+
"set_auth_settings",
|
|
37
|
+
"reset_auth_settings",
|
|
38
|
+
# Validation
|
|
39
|
+
"AuthValidationFailure",
|
|
40
|
+
"AuthValidationSuccess",
|
|
41
|
+
"JWTTokenValidator",
|
|
42
|
+
"SAMLTokenValidator",
|
|
43
|
+
"TokenValidator",
|
|
44
|
+
"OIDCTokenValidator",
|
|
45
|
+
"ValidationErrorKind",
|
|
46
|
+
"MaltegoAuthErrorCode",
|
|
47
|
+
"MaltegoAuthProblemDetail",
|
|
48
|
+
"AuthProblemException",
|
|
49
|
+
"AuthContext",
|
|
50
|
+
"Identity",
|
|
51
|
+
]
|
maltego/auth/claims.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Copyright (c) Maltego Technologies GmbH.
|
|
2
|
+
"""JWT claim helpers."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import base64
|
|
7
|
+
import binascii
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
_BASE64URL_SEGMENT = re.compile(r"^[A-Za-z0-9_-]*$")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _decode_base64url_json(segment: str) -> Optional[Any]:
|
|
16
|
+
if not segment:
|
|
17
|
+
return None
|
|
18
|
+
try:
|
|
19
|
+
padding = "=" * (-len(segment) % 4)
|
|
20
|
+
decoded = base64.urlsafe_b64decode(f"{segment}{padding}".encode("ascii"))
|
|
21
|
+
return json.loads(decoded.decode("utf-8"))
|
|
22
|
+
except (binascii.Error, UnicodeDecodeError, ValueError):
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def decode_unverified_jwt_claims(token: str) -> Optional[dict[str, Any]]:
|
|
27
|
+
"""Decode compact JWT payload claims without signature or claim validation."""
|
|
28
|
+
parts = token.split(".")
|
|
29
|
+
if len(parts) != 3 or any(_BASE64URL_SEGMENT.fullmatch(part) is None for part in parts):
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
header = _decode_base64url_json(parts[0])
|
|
33
|
+
payload = _decode_base64url_json(parts[1])
|
|
34
|
+
if not isinstance(header, dict) or not isinstance(payload, dict):
|
|
35
|
+
return None
|
|
36
|
+
return payload
|