@untangle-code/file-graph 0.0.0-stage → 0.1.0
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.
- package/LICENSE +661 -0
- package/NOTICE +90 -0
- package/README.md +284 -2
- package/THIRD_PARTY_LICENSES.md +29 -0
- package/dist/assignment.d.ts +15 -0
- package/dist/assignment.d.ts.map +1 -0
- package/dist/assignment.js +75 -0
- package/dist/assignment.js.map +1 -0
- package/dist/boundaries/endpoints.d.ts +11 -0
- package/dist/boundaries/endpoints.d.ts.map +1 -0
- package/dist/boundaries/endpoints.js +195 -0
- package/dist/boundaries/endpoints.js.map +1 -0
- package/dist/boundaries/index.d.ts +9 -0
- package/dist/boundaries/index.d.ts.map +1 -0
- package/dist/boundaries/index.js +93 -0
- package/dist/boundaries/index.js.map +1 -0
- package/dist/boundaries/requests.d.ts +12 -0
- package/dist/boundaries/requests.d.ts.map +1 -0
- package/dist/boundaries/requests.js +311 -0
- package/dist/boundaries/requests.js.map +1 -0
- package/dist/edges.d.ts +49 -0
- package/dist/edges.d.ts.map +1 -0
- package/dist/edges.js +223 -0
- package/dist/edges.js.map +1 -0
- package/dist/file-index.d.ts +37 -0
- package/dist/file-index.d.ts.map +1 -0
- package/dist/file-index.js +103 -0
- package/dist/file-index.js.map +1 -0
- package/dist/graph.d.ts +160 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +448 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics.d.ts +67 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +272 -0
- package/dist/metrics.js.map +1 -0
- package/dist/package-catalog.d.ts +43 -0
- package/dist/package-catalog.d.ts.map +1 -0
- package/dist/package-catalog.js +136 -0
- package/dist/package-catalog.js.map +1 -0
- package/dist/paths.d.ts +56 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +129 -0
- package/dist/paths.js.map +1 -0
- package/dist/persistence.d.ts +83 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +538 -0
- package/dist/persistence.js.map +1 -0
- package/dist/resolve/candidates.d.ts +52 -0
- package/dist/resolve/candidates.d.ts.map +1 -0
- package/dist/resolve/candidates.js +202 -0
- package/dist/resolve/candidates.js.map +1 -0
- package/dist/resolve/package-exports.d.ts +31 -0
- package/dist/resolve/package-exports.d.ts.map +1 -0
- package/dist/resolve/package-exports.js +229 -0
- package/dist/resolve/package-exports.js.map +1 -0
- package/dist/resolve/pipeline.d.ts +11 -0
- package/dist/resolve/pipeline.d.ts.map +1 -0
- package/dist/resolve/pipeline.js +109 -0
- package/dist/resolve/pipeline.js.map +1 -0
- package/dist/resolve/resolvers.d.ts +25 -0
- package/dist/resolve/resolvers.d.ts.map +1 -0
- package/dist/resolve/resolvers.js +367 -0
- package/dist/resolve/resolvers.js.map +1 -0
- package/dist/restore.d.ts +75 -0
- package/dist/restore.d.ts.map +1 -0
- package/dist/restore.js +136 -0
- package/dist/restore.js.map +1 -0
- package/dist/topology/compose.d.ts +27 -0
- package/dist/topology/compose.d.ts.map +1 -0
- package/dist/topology/compose.js +208 -0
- package/dist/topology/compose.js.map +1 -0
- package/dist/topology/jsonc.d.ts +38 -0
- package/dist/topology/jsonc.d.ts.map +1 -0
- package/dist/topology/jsonc.js +137 -0
- package/dist/topology/jsonc.js.map +1 -0
- package/dist/topology/load.d.ts +61 -0
- package/dist/topology/load.d.ts.map +1 -0
- package/dist/topology/load.js +436 -0
- package/dist/topology/load.js.map +1 -0
- package/dist/topology/manifest.d.ts +48 -0
- package/dist/topology/manifest.d.ts.map +1 -0
- package/dist/topology/manifest.js +109 -0
- package/dist/topology/manifest.js.map +1 -0
- package/dist/topology/project-config.d.ts +39 -0
- package/dist/topology/project-config.d.ts.map +1 -0
- package/dist/topology/project-config.js +150 -0
- package/dist/topology/project-config.js.map +1 -0
- package/dist/topology/workspace-definition.d.ts +26 -0
- package/dist/topology/workspace-definition.d.ts.map +1 -0
- package/dist/topology/workspace-definition.js +104 -0
- package/dist/topology/workspace-definition.js.map +1 -0
- package/dist/traverse.d.ts +49 -0
- package/dist/traverse.d.ts.map +1 -0
- package/dist/traverse.js +167 -0
- package/dist/traverse.js.map +1 -0
- package/dist/types.d.ts +332 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -3
- package/src/assignment.ts +83 -0
- package/src/boundaries/endpoints.ts +273 -0
- package/src/boundaries/index.ts +118 -0
- package/src/boundaries/requests.ts +411 -0
- package/src/edges.ts +334 -0
- package/src/file-index.ts +127 -0
- package/src/graph.ts +591 -0
- package/src/index.ts +58 -0
- package/src/metrics.ts +350 -0
- package/src/package-catalog.ts +145 -0
- package/src/paths.ts +138 -0
- package/src/persistence.ts +746 -0
- package/src/resolve/candidates.ts +230 -0
- package/src/resolve/package-exports.ts +268 -0
- package/src/resolve/pipeline.ts +140 -0
- package/src/resolve/resolvers.ts +468 -0
- package/src/restore.ts +201 -0
- package/src/topology/compose.ts +268 -0
- package/src/topology/jsonc.ts +147 -0
- package/src/topology/load.ts +644 -0
- package/src/topology/manifest.ts +132 -0
- package/src/topology/project-config.ts +183 -0
- package/src/topology/workspace-definition.ts +115 -0
- package/src/traverse.ts +232 -0
- package/src/types.ts +372 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { UntangleError } from "@untangle-code/domain";
|
|
2
|
+
export function under(path, root) {
|
|
3
|
+
const normalized = root.replace(/\/$/, "");
|
|
4
|
+
return normalized === "" || path === normalized || path.startsWith(`${normalized}/`);
|
|
5
|
+
}
|
|
6
|
+
export function provenance(file, range, kind, name) {
|
|
7
|
+
return {
|
|
8
|
+
path: file.path,
|
|
9
|
+
contentHash: file.contentHash,
|
|
10
|
+
range,
|
|
11
|
+
kind,
|
|
12
|
+
...(name === undefined ? {} : { name }),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function key(ref) {
|
|
16
|
+
return `${ref.file.path}:${ref.router.scope}:${ref.router.name}`;
|
|
17
|
+
}
|
|
18
|
+
function enclosingScopes(file, scope) {
|
|
19
|
+
const result = [scope];
|
|
20
|
+
let current = scope;
|
|
21
|
+
for (let depth = 0; depth < 30 && current !== "module"; depth += 1) {
|
|
22
|
+
current = file.functions.find((fn) => fn.scope === current)?.parentScope ?? "module";
|
|
23
|
+
result.push(current);
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
function localRouter(file, name, scope) {
|
|
28
|
+
for (const candidateScope of enclosingScopes(file, scope)) {
|
|
29
|
+
const candidates = file.routers.filter((router) => router.name === name && router.scope === candidateScope);
|
|
30
|
+
const bindings = file.bindings.filter((binding) => binding.scope === candidateScope && binding.name === name);
|
|
31
|
+
const fn = file.functions.find((fn) => fn.scope === candidateScope);
|
|
32
|
+
if (bindings.length > 1 || fn?.parameters.some((param) => param.name === name))
|
|
33
|
+
return null;
|
|
34
|
+
if (bindings.length > 0 && candidates.length === 0)
|
|
35
|
+
return null;
|
|
36
|
+
if (candidates.length === 1)
|
|
37
|
+
return { file, router: candidates[0] };
|
|
38
|
+
if (candidates.length > 1)
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function importedModulePath(from, module) {
|
|
44
|
+
if (!module.startsWith("."))
|
|
45
|
+
return module.replaceAll(".", "/");
|
|
46
|
+
const dots = module.match(/^\.+/)?.[0].length ?? 0;
|
|
47
|
+
const dirs = from.split("/").slice(0, -dots);
|
|
48
|
+
return [...dirs, module.slice(dots).replaceAll(".", "/")].filter(Boolean).join("/");
|
|
49
|
+
}
|
|
50
|
+
function includedRouter(files, file, name, scope) {
|
|
51
|
+
const local = localRouter(file, name, scope);
|
|
52
|
+
if (local !== null)
|
|
53
|
+
return local;
|
|
54
|
+
const segments = name.split(".");
|
|
55
|
+
const base = segments[0];
|
|
56
|
+
if (enclosingScopes(file, scope).some((candidateScope) => file.functions
|
|
57
|
+
.find((fn) => fn.scope === candidateScope)
|
|
58
|
+
?.parameters.some((parameter) => parameter.name === base) ||
|
|
59
|
+
file.bindings.some((binding) => binding.scope === candidateScope && binding.name === base) ||
|
|
60
|
+
file.functions.some((fn) => fn.parentScope === candidateScope && fn.name === base)))
|
|
61
|
+
return null;
|
|
62
|
+
const imported = file.pythonImports.filter((item) => item.local === base);
|
|
63
|
+
if (imported.length !== 1)
|
|
64
|
+
return null;
|
|
65
|
+
const item = imported[0];
|
|
66
|
+
if (item === undefined)
|
|
67
|
+
return null;
|
|
68
|
+
const directRouter = segments.length === 1;
|
|
69
|
+
const module = directRouter ? item.module : `${item.module}.${item.imported ?? item.local}`;
|
|
70
|
+
const modulePath = importedModulePath(file.path, module);
|
|
71
|
+
const candidates = files.filter((candidate) => candidate.path === `${modulePath}.py` || candidate.path.endsWith(`/${modulePath}.py`));
|
|
72
|
+
if (candidates.length !== 1)
|
|
73
|
+
return null;
|
|
74
|
+
const target = candidates[0];
|
|
75
|
+
return target === undefined
|
|
76
|
+
? null
|
|
77
|
+
: localRouter(target, directRouter ? (item.imported ?? "router") : segments.slice(1).join("."), "module");
|
|
78
|
+
}
|
|
79
|
+
function routePath(...parts) {
|
|
80
|
+
if (parts.some((part) => part === null))
|
|
81
|
+
return null;
|
|
82
|
+
const result = parts.join("");
|
|
83
|
+
if (!result.startsWith("/") ||
|
|
84
|
+
result.includes("//") ||
|
|
85
|
+
result.includes("?") ||
|
|
86
|
+
result.includes("#"))
|
|
87
|
+
return null;
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
export function collectEndpoints(files, services, signal) {
|
|
91
|
+
const endpoints = [], limitations = [];
|
|
92
|
+
const mounted = new Set();
|
|
93
|
+
let visits = 0;
|
|
94
|
+
const visit = (ref, prefix, chain, seen, serviceId) => {
|
|
95
|
+
if (signal?.aborted)
|
|
96
|
+
throw new UntangleError("JOB_CANCELLED", "HTTP endpoint collection cancelled");
|
|
97
|
+
visits += 1;
|
|
98
|
+
if (visits > 10000 || endpoints.length > 20000)
|
|
99
|
+
throw new UntangleError("ANALYSIS_INCOMPLETE", "HTTP router expansion budget exceeded");
|
|
100
|
+
if (seen.size >= 100) {
|
|
101
|
+
limitations.push({ path: ref.file.path, reason: "Router include depth budget exceeded" });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (seen.has(key(ref))) {
|
|
105
|
+
limitations.push({
|
|
106
|
+
path: ref.file.path,
|
|
107
|
+
reason: `Router include cycle at ${ref.router.name}`,
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
mounted.add(key(ref));
|
|
112
|
+
const nextSeen = new Set([...seen, key(ref)]);
|
|
113
|
+
const routerPrefix = routePath(prefix, ref.router.prefix);
|
|
114
|
+
const nextPrefix = prefix === "" && ref.router.prefix === "" ? "" : routerPrefix;
|
|
115
|
+
const provenanceChain = [
|
|
116
|
+
...chain,
|
|
117
|
+
provenance(ref.file, ref.router.range, "router", ref.router.name),
|
|
118
|
+
];
|
|
119
|
+
for (const route of ref.file.routes.filter((route) => route.router === ref.router.name &&
|
|
120
|
+
localRouter(ref.file, route.router, route.scope)?.router === ref.router)) {
|
|
121
|
+
const path = routePath(nextPrefix, route.path);
|
|
122
|
+
if (path === null)
|
|
123
|
+
limitations.push({
|
|
124
|
+
path: ref.file.path,
|
|
125
|
+
reason: `Dynamic or invalid route prefix/path for ${route.handler}`,
|
|
126
|
+
});
|
|
127
|
+
else {
|
|
128
|
+
if (/\{[^}]+:(?!str\})[^}]+\}/.test(path))
|
|
129
|
+
limitations.push({
|
|
130
|
+
path: ref.file.path,
|
|
131
|
+
reason: `Typed or catch-all route converters are not matched: ${path}`,
|
|
132
|
+
});
|
|
133
|
+
endpoints.push({
|
|
134
|
+
serviceId,
|
|
135
|
+
method: route.method,
|
|
136
|
+
path,
|
|
137
|
+
handler: route.handler,
|
|
138
|
+
provenance: [
|
|
139
|
+
...provenanceChain,
|
|
140
|
+
provenance(ref.file, route.range, "route", route.handler),
|
|
141
|
+
],
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (const include of ref.file.includes.filter((include) => localRouter(ref.file, include.owner, include.scope)?.router === ref.router)) {
|
|
146
|
+
const target = includedRouter(files, ref.file, include.router, include.scope);
|
|
147
|
+
if (target === null) {
|
|
148
|
+
limitations.push({
|
|
149
|
+
path: ref.file.path,
|
|
150
|
+
reason: `Unresolved router include ${include.router}`,
|
|
151
|
+
});
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (serviceId !== null &&
|
|
155
|
+
!services.some((service) => service.id === serviceId &&
|
|
156
|
+
service.backendRoots.some((root) => under(target.file.path, root)))) {
|
|
157
|
+
limitations.push({
|
|
158
|
+
path: ref.file.path,
|
|
159
|
+
reason: `Router include ${include.router} lies outside declared backend roots`,
|
|
160
|
+
});
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const includePrefix = nextPrefix === "" && include.prefix === "" ? "" : routePath(nextPrefix, include.prefix);
|
|
164
|
+
visit(target, includePrefix, [...provenanceChain, provenance(ref.file, include.range, "include", include.router)], nextSeen, serviceId);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
for (const file of files) {
|
|
168
|
+
const unsafeMounts = files.filter((candidate) => candidate.limitations.some((reason) => reason.startsWith("ASGI app.mount")));
|
|
169
|
+
const unsafeService = unsafeMounts.some((unsafe) => services.length === 0 ||
|
|
170
|
+
services.some((service) => service.backendRoots.some((root) => under(file.path, root)) &&
|
|
171
|
+
service.backendRoots.some((root) => under(unsafe.path, root))));
|
|
172
|
+
if (unsafeService) {
|
|
173
|
+
limitations.push({
|
|
174
|
+
path: file.path,
|
|
175
|
+
reason: "App endpoint inventory suppressed because ASGI mount routing is unsupported",
|
|
176
|
+
});
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
for (const router of file.routers.filter((router) => router.kind === "app")) {
|
|
180
|
+
const matching = services.filter((service) => service.backendRoots.some((root) => under(file.path, root)));
|
|
181
|
+
for (const service of matching.length > 0 ? matching : [null])
|
|
182
|
+
visit({ file, router }, "", [], new Set(), service?.id ?? null);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const file of files)
|
|
186
|
+
for (const router of file.routers) {
|
|
187
|
+
if (router.kind === "router" && !mounted.has(key({ file, router })))
|
|
188
|
+
limitations.push({
|
|
189
|
+
path: file.path,
|
|
190
|
+
reason: `Router ${router.name} is not proven mounted on a FastAPI app`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return { endpoints, limitations };
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=endpoints.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"endpoints.js","sourceRoot":"","sources":["../../src/boundaries/endpoints.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HttpBoundaryFileFactsV1, HttpBoundaryReportV1, HttpServiceBindingV1 } from "@untangle-code/domain";
|
|
2
|
+
export interface AnalyzeHttpBoundariesOptions {
|
|
3
|
+
files: readonly HttpBoundaryFileFactsV1[];
|
|
4
|
+
services: readonly HttpServiceBindingV1[];
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
}
|
|
7
|
+
/** Resolve declared deployment scopes plus method/path evidence, never bare URL coincidences. */
|
|
8
|
+
export declare function analyzeHttpBoundaries(options: AnalyzeHttpBoundariesOptions): HttpBoundaryReportV1;
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/boundaries/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,uBAAuB,EACvB,oBAAoB,EAGpB,oBAAoB,EACrB,MAAM,uBAAuB,CAAC;AAM/B,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,SAAS,uBAAuB,EAAE,CAAC;IAC1C,QAAQ,EAAE,SAAS,oBAAoB,EAAE,CAAC;IAC1C,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAgCD,iGAAiG;AACjG,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,oBAAoB,CAoEjG"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { compareCodepoints, UntangleError } from "@untangle-code/domain";
|
|
2
|
+
import { collectEndpoints, under } from "./endpoints.js";
|
|
3
|
+
import { collectRequests } from "./requests.js";
|
|
4
|
+
function checkCancelled(signal) {
|
|
5
|
+
if (signal?.aborted)
|
|
6
|
+
throw new UntangleError("JOB_CANCELLED", "HTTP boundary analysis cancelled");
|
|
7
|
+
}
|
|
8
|
+
function matchesPath(request, route) {
|
|
9
|
+
const requested = request.split("/"), target = route.split("/");
|
|
10
|
+
if (requested.length !== target.length)
|
|
11
|
+
return false;
|
|
12
|
+
return target.every((segment, index) => {
|
|
13
|
+
const value = requested[index] ?? "";
|
|
14
|
+
if (/^\{[A-Za-z_][A-Za-z0-9_]*(?::str)?\}$/.test(segment))
|
|
15
|
+
return value !== "";
|
|
16
|
+
return segment === value && value !== "{*}";
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function serviceApplies(request, service) {
|
|
20
|
+
return request.origin === null
|
|
21
|
+
? service.sameOrigin === true
|
|
22
|
+
: (service.origins ?? []).some((origin) => {
|
|
23
|
+
try {
|
|
24
|
+
return new URL(origin).origin === request.origin && new URL(origin).pathname === "/";
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** Resolve declared deployment scopes plus method/path evidence, never bare URL coincidences. */
|
|
32
|
+
export function analyzeHttpBoundaries(options) {
|
|
33
|
+
checkCancelled(options.signal);
|
|
34
|
+
const files = [...options.files].sort((a, b) => compareCodepoints(a.path, b.path));
|
|
35
|
+
const services = [...options.services].sort((a, b) => compareCodepoints(a.id, b.id));
|
|
36
|
+
if (new Set(services.map((service) => service.id)).size !== services.length)
|
|
37
|
+
throw new TypeError("HTTP service identifiers must be unique");
|
|
38
|
+
if (new Set(files.map((file) => file.path)).size !== files.length)
|
|
39
|
+
throw new TypeError("HTTP source paths must be unique");
|
|
40
|
+
const { endpoints, limitations } = collectEndpoints(files, services, options.signal);
|
|
41
|
+
const requests = [];
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
checkCancelled(options.signal);
|
|
44
|
+
limitations.push(...file.limitations.map((reason) => ({ path: file.path, reason })));
|
|
45
|
+
if (file.languageId === "python")
|
|
46
|
+
continue;
|
|
47
|
+
for (const request of collectRequests(file)) {
|
|
48
|
+
checkCancelled(options.signal);
|
|
49
|
+
const applicable = services.filter((service) => service.frontendRoots.some((root) => under(file.path, root)) &&
|
|
50
|
+
serviceApplies(request, service));
|
|
51
|
+
const serviceIds = applicable.map((service) => service.id);
|
|
52
|
+
const targets = request.reason === undefined && request.path !== null && request.method !== null
|
|
53
|
+
? endpoints.filter((endpoint) => endpoint.serviceId !== null &&
|
|
54
|
+
serviceIds.includes(endpoint.serviceId) &&
|
|
55
|
+
endpoint.method === request.method &&
|
|
56
|
+
matchesPath(request.path, endpoint.path))
|
|
57
|
+
: [];
|
|
58
|
+
const status = targets.length === 1 ? "resolved" : targets.length > 1 ? "ambiguous" : "unresolved";
|
|
59
|
+
const reason = request.reason ??
|
|
60
|
+
(applicable.length === 0
|
|
61
|
+
? "No declared service binding for the request origin and frontend root"
|
|
62
|
+
: targets.length === 0
|
|
63
|
+
? "No mounted route matches the HTTP method and normalized path"
|
|
64
|
+
: targets.length > 1
|
|
65
|
+
? "Multiple mounted routes match; no target was selected"
|
|
66
|
+
: undefined);
|
|
67
|
+
requests.push({
|
|
68
|
+
...request,
|
|
69
|
+
status,
|
|
70
|
+
...(reason === undefined ? {} : { reason }),
|
|
71
|
+
serviceIds,
|
|
72
|
+
targets,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
schemaVersion: 1,
|
|
78
|
+
kind: "http-boundaries",
|
|
79
|
+
services,
|
|
80
|
+
endpoints,
|
|
81
|
+
requests,
|
|
82
|
+
limitations,
|
|
83
|
+
summary: {
|
|
84
|
+
files: files.length,
|
|
85
|
+
endpoints: endpoints.length,
|
|
86
|
+
requests: requests.length,
|
|
87
|
+
resolved: requests.filter((request) => request.status === "resolved").length,
|
|
88
|
+
ambiguous: requests.filter((request) => request.status === "ambiguous").length,
|
|
89
|
+
unresolved: requests.filter((request) => request.status === "unresolved").length,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/boundaries/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HttpBoundaryFileFactsV1, HttpBoundaryProvenanceV1 } from "@untangle-code/domain";
|
|
2
|
+
export interface ExtractedRequest {
|
|
3
|
+
source: HttpBoundaryProvenanceV1;
|
|
4
|
+
method: string | null;
|
|
5
|
+
path: string | null;
|
|
6
|
+
origin: string | null;
|
|
7
|
+
reason?: string;
|
|
8
|
+
provenance: HttpBoundaryProvenanceV1[];
|
|
9
|
+
}
|
|
10
|
+
/** Bounded, same-file wrapper substitution; no whole-program execution or guessed clients. */
|
|
11
|
+
export declare function collectRequests(file: HttpBoundaryFileFactsV1): ExtractedRequest[];
|
|
12
|
+
//# sourceMappingURL=requests.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requests.d.ts","sourceRoot":"","sources":["../../src/boundaries/requests.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,uBAAuB,EACvB,wBAAwB,EAIzB,MAAM,uBAAuB,CAAC;AA8R/B,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,wBAAwB,CAAC;IACjC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,wBAAwB,EAAE,CAAC;CACxC;AAED,8FAA8F;AAC9F,wBAAgB,eAAe,CAAC,IAAI,EAAE,uBAAuB,GAAG,gBAAgB,EAAE,CA4GjF"}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { UntangleError } from "@untangle-code/domain";
|
|
2
|
+
import { provenance } from "./endpoints.js";
|
|
3
|
+
const UNKNOWN = { kind: "unknown", detail: "unbound value" };
|
|
4
|
+
const UNDEFINED = { kind: "undefined" };
|
|
5
|
+
const DYNAMIC = "«dynamic»";
|
|
6
|
+
const ENCODED_SEGMENT = "«encoded-segment»";
|
|
7
|
+
function functionAt(file, scope) {
|
|
8
|
+
return file.functions.find((fn) => fn.scope === scope);
|
|
9
|
+
}
|
|
10
|
+
function referencesName(expr, name, depth = 0) {
|
|
11
|
+
if (depth > 40)
|
|
12
|
+
return true;
|
|
13
|
+
if (expr.kind === "name")
|
|
14
|
+
return expr.name === name;
|
|
15
|
+
if (expr.kind === "template")
|
|
16
|
+
return expr.parts.some((part) => typeof part !== "string" && referencesName(part, name, depth + 1));
|
|
17
|
+
if (expr.kind === "object")
|
|
18
|
+
return expr.entries.some((entry) => referencesName("spread" in entry ? entry.spread : entry.value, name, depth + 1));
|
|
19
|
+
if (expr.kind === "member")
|
|
20
|
+
return referencesName(expr.object, name, depth + 1);
|
|
21
|
+
if (expr.kind === "call")
|
|
22
|
+
return expr.arguments.some((arg) => referencesName(arg, name, depth + 1));
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
/** Fail closed when an options object can escape to code whose writes are unknown. */
|
|
26
|
+
function objectEscapes(name, context, seen = new Set()) {
|
|
27
|
+
const location = `${context.scope}:${name}`;
|
|
28
|
+
if (seen.has(location) || seen.size >= 12)
|
|
29
|
+
return true;
|
|
30
|
+
const next = new Set([...seen, location]);
|
|
31
|
+
if (context.file.bindings.some((binding) => binding.scope === context.scope &&
|
|
32
|
+
binding.name !== name &&
|
|
33
|
+
referencesName(binding.value, name)))
|
|
34
|
+
return true;
|
|
35
|
+
if (context.file.bindings.some((binding) => binding.scope === context.scope &&
|
|
36
|
+
binding.name === name &&
|
|
37
|
+
binding.value.kind === "unknown" &&
|
|
38
|
+
binding.value.detail === "reassigned binding"))
|
|
39
|
+
return true;
|
|
40
|
+
for (const call of context.file.calls.filter((call) => call.scope === context.scope)) {
|
|
41
|
+
for (let index = 0; index < call.arguments.length; index += 1) {
|
|
42
|
+
const argument = call.arguments[index];
|
|
43
|
+
if (argument?.kind !== "name" || argument.name !== name)
|
|
44
|
+
continue;
|
|
45
|
+
if (transportName(context.file, call))
|
|
46
|
+
continue;
|
|
47
|
+
const target = localFunction(context.file, call);
|
|
48
|
+
const parameter = target?.parameters[index];
|
|
49
|
+
if (target === null ||
|
|
50
|
+
target === undefined ||
|
|
51
|
+
parameter === undefined ||
|
|
52
|
+
objectEscapes(parameter.name, { ...context, scope: target.scope, env: new Map() }, next))
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
function resolveName(name, context, seen, depth) {
|
|
59
|
+
const lookup = `${context.scope}:${name}`;
|
|
60
|
+
if (seen.has(lookup))
|
|
61
|
+
return UNKNOWN;
|
|
62
|
+
const next = new Set([...seen, lookup]);
|
|
63
|
+
const binding = context.file.bindings.filter((item) => item.scope === context.scope && item.name === name);
|
|
64
|
+
if (binding.length > 1)
|
|
65
|
+
return { kind: "unknown", detail: "multiple or reassigned bindings" };
|
|
66
|
+
if (binding.length === 1 && binding[0]?.value.kind === "object" && objectEscapes(name, context))
|
|
67
|
+
return { kind: "unknown", detail: "object binding escapes static transport analysis" };
|
|
68
|
+
if (binding.length === 1)
|
|
69
|
+
return resolveExpression(binding[0].value, context, next, depth + 1);
|
|
70
|
+
const envValue = context.env.get(name);
|
|
71
|
+
if (envValue !== undefined)
|
|
72
|
+
return envValue.kind === "object" && objectEscapes(name, context)
|
|
73
|
+
? { kind: "unknown", detail: "object parameter escapes static transport analysis" }
|
|
74
|
+
: envValue;
|
|
75
|
+
const fn = functionAt(context.file, context.scope);
|
|
76
|
+
if (fn?.parameters.some((param) => param.name === name))
|
|
77
|
+
return UNKNOWN;
|
|
78
|
+
if (context.scope !== "module")
|
|
79
|
+
return resolveName(name, { ...context, scope: fn?.parentScope ?? "module", env: new Map() }, next, depth + 1);
|
|
80
|
+
return { kind: "name", name };
|
|
81
|
+
}
|
|
82
|
+
function resolveExpression(expr, context, seen = new Set(), depth = 0) {
|
|
83
|
+
if (depth > 40)
|
|
84
|
+
return UNKNOWN;
|
|
85
|
+
if (expr.kind === "name")
|
|
86
|
+
return resolveName(expr.name, context, seen, depth);
|
|
87
|
+
const resolve = (value) => resolveExpression(value, context, seen, depth + 1);
|
|
88
|
+
if (expr.kind === "template")
|
|
89
|
+
return {
|
|
90
|
+
kind: "template",
|
|
91
|
+
parts: expr.parts.map((part) => (typeof part === "string" ? part : resolve(part))),
|
|
92
|
+
};
|
|
93
|
+
if (expr.kind === "object")
|
|
94
|
+
return {
|
|
95
|
+
kind: "object",
|
|
96
|
+
entries: expr.entries.map((entry) => "spread" in entry
|
|
97
|
+
? { spread: resolve(entry.spread) }
|
|
98
|
+
: { key: entry.key, value: resolve(entry.value) }),
|
|
99
|
+
};
|
|
100
|
+
if (expr.kind === "member")
|
|
101
|
+
return { ...expr, object: resolve(expr.object) };
|
|
102
|
+
if (expr.kind === "call") {
|
|
103
|
+
if (expr.callee.kind === "name" && expr.callee.name === "encodeURIComponent") {
|
|
104
|
+
const shadowed = scopeChain(context.file, context.scope).some((scope) => context.file.bindings.some((binding) => binding.scope === scope && binding.name === "encodeURIComponent") ||
|
|
105
|
+
functionAt(context.file, scope)?.parameters.some((param) => param.name === "encodeURIComponent") ||
|
|
106
|
+
context.file.functions.some((fn) => fn.parentScope === scope && fn.name === "encodeURIComponent")) || context.file.imports.some((raw) => raw.importedNames?.includes("encodeURIComponent"));
|
|
107
|
+
if (shadowed)
|
|
108
|
+
return UNKNOWN;
|
|
109
|
+
}
|
|
110
|
+
return { ...expr, arguments: expr.arguments.map(resolve) };
|
|
111
|
+
}
|
|
112
|
+
return expr;
|
|
113
|
+
}
|
|
114
|
+
function scopeChain(file, scope) {
|
|
115
|
+
const result = [scope];
|
|
116
|
+
for (let depth = 0; depth < 30 && scope !== "module"; depth += 1) {
|
|
117
|
+
scope = functionAt(file, scope)?.parentScope ?? "module";
|
|
118
|
+
result.push(scope);
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
function transportName(file, call) {
|
|
123
|
+
if (call.callee.kind !== "name")
|
|
124
|
+
return false;
|
|
125
|
+
const name = call.callee.name;
|
|
126
|
+
for (const scope of scopeChain(file, call.scope)) {
|
|
127
|
+
if (file.bindings.some((binding) => binding.scope === scope && binding.name === name))
|
|
128
|
+
return false;
|
|
129
|
+
const fn = functionAt(file, scope);
|
|
130
|
+
const parameter = fn?.parameters.find((param) => param.name === name);
|
|
131
|
+
if (parameter !== undefined)
|
|
132
|
+
return parameter.fetchTransport === true;
|
|
133
|
+
if (file.functions.some((candidate) => candidate.parentScope === scope && candidate.name === name))
|
|
134
|
+
return false;
|
|
135
|
+
if (file.imports.some((raw) => raw.importedNames?.includes(name)))
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return name === "fetch";
|
|
139
|
+
}
|
|
140
|
+
function localFunction(file, call) {
|
|
141
|
+
if (call.callee.kind !== "name")
|
|
142
|
+
return null;
|
|
143
|
+
const name = call.callee.name;
|
|
144
|
+
for (const scope of scopeChain(file, call.scope)) {
|
|
145
|
+
const candidates = file.functions.filter((fn) => fn.parentScope === scope && fn.name === name);
|
|
146
|
+
if (candidates.length === 1)
|
|
147
|
+
return candidates[0];
|
|
148
|
+
if (candidates.length > 1 ||
|
|
149
|
+
file.bindings.some((binding) => binding.scope === scope && binding.name === name))
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
function render(expr) {
|
|
155
|
+
if (expr.kind === "string")
|
|
156
|
+
return expr.value.includes(DYNAMIC) || expr.value.includes(ENCODED_SEGMENT)
|
|
157
|
+
? DYNAMIC
|
|
158
|
+
: expr.value;
|
|
159
|
+
if (expr.kind === "template")
|
|
160
|
+
return expr.parts.map((part) => (typeof part === "string" ? part : render(part))).join("");
|
|
161
|
+
if (expr.kind === "call" &&
|
|
162
|
+
expr.callee.kind === "name" &&
|
|
163
|
+
expr.callee.name === "encodeURIComponent" &&
|
|
164
|
+
expr.arguments.length === 1)
|
|
165
|
+
return ENCODED_SEGMENT;
|
|
166
|
+
return DYNAMIC;
|
|
167
|
+
}
|
|
168
|
+
function methodOf(expr) {
|
|
169
|
+
if (expr.kind === "undefined")
|
|
170
|
+
return "GET";
|
|
171
|
+
if (expr.kind !== "object")
|
|
172
|
+
return null;
|
|
173
|
+
let method = UNDEFINED;
|
|
174
|
+
for (const entry of expr.entries) {
|
|
175
|
+
if ("spread" in entry) {
|
|
176
|
+
if (entry.spread.kind === "object") {
|
|
177
|
+
const nested = methodOf(entry.spread);
|
|
178
|
+
if (nested !== "GET" ||
|
|
179
|
+
entry.spread.entries.some((item) => "key" in item && item.key === "method"))
|
|
180
|
+
method = nested === null ? UNKNOWN : { kind: "string", value: nested };
|
|
181
|
+
}
|
|
182
|
+
else if (entry.spread.kind !== "undefined")
|
|
183
|
+
method = UNKNOWN;
|
|
184
|
+
}
|
|
185
|
+
else if (entry.key === "method")
|
|
186
|
+
method = entry.value;
|
|
187
|
+
}
|
|
188
|
+
if (method.kind === "undefined")
|
|
189
|
+
return "GET";
|
|
190
|
+
return method.kind === "string" && /^[A-Za-z]+$/.test(method.value)
|
|
191
|
+
? method.value.toUpperCase()
|
|
192
|
+
: null;
|
|
193
|
+
}
|
|
194
|
+
function normalizeRequest(url) {
|
|
195
|
+
const bare = url.split(/[?#]/, 1)[0] ?? "";
|
|
196
|
+
if (bare.includes(DYNAMIC))
|
|
197
|
+
return { path: null, origin: null, reason: "unbounded dynamic URL or path" };
|
|
198
|
+
let path = bare, origin = null;
|
|
199
|
+
if (/^https?:\/\//i.test(bare)) {
|
|
200
|
+
try {
|
|
201
|
+
const parsed = new URL(bare);
|
|
202
|
+
if (parsed.username || parsed.password)
|
|
203
|
+
return { path: null, origin: null, reason: "URL credentials are unsupported" };
|
|
204
|
+
origin = parsed.origin;
|
|
205
|
+
const authorityEnd = bare.indexOf("/", bare.indexOf("://") + 3);
|
|
206
|
+
path = authorityEnd === -1 ? "/" : bare.slice(authorityEnd);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return { path: null, origin: null, reason: "invalid absolute URL" };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (!path.startsWith("/") ||
|
|
213
|
+
path.startsWith("//") ||
|
|
214
|
+
path.includes("\\") ||
|
|
215
|
+
path.includes("//") ||
|
|
216
|
+
/%(?:2e|2f|5c)/i.test(path) ||
|
|
217
|
+
/(^|\/)\.{1,2}(\/|$)/.test(path))
|
|
218
|
+
return {
|
|
219
|
+
path: null,
|
|
220
|
+
origin,
|
|
221
|
+
reason: "URL is not a normalized root-relative or absolute HTTP URL",
|
|
222
|
+
};
|
|
223
|
+
if (path.split("/").some((part) => part.includes(ENCODED_SEGMENT) && part !== ENCODED_SEGMENT))
|
|
224
|
+
return { path: null, origin, reason: "dynamic segment has no complete literal boundary" };
|
|
225
|
+
return { path: path.replaceAll(ENCODED_SEGMENT, "{*}"), origin };
|
|
226
|
+
}
|
|
227
|
+
/** Bounded, same-file wrapper substitution; no whole-program execution or guessed clients. */
|
|
228
|
+
export function collectRequests(file) {
|
|
229
|
+
const result = [];
|
|
230
|
+
const transportCalls = file.calls.filter((call) => transportName(file, call));
|
|
231
|
+
const wrappers = new Set(transportCalls.map((call) => call.scope));
|
|
232
|
+
for (let iteration = 0; iteration < 8; iteration += 1) {
|
|
233
|
+
let changed = false;
|
|
234
|
+
for (const call of file.calls) {
|
|
235
|
+
const target = localFunction(file, call);
|
|
236
|
+
if (target !== null &&
|
|
237
|
+
target.parameters.length > 0 &&
|
|
238
|
+
wrappers.has(target.scope) &&
|
|
239
|
+
!wrappers.has(call.scope)) {
|
|
240
|
+
wrappers.add(call.scope);
|
|
241
|
+
changed = true;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (!changed)
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
const emit = (call, context, source, chain) => {
|
|
248
|
+
if (result.length >= 10000)
|
|
249
|
+
throw new UntangleError("ANALYSIS_INCOMPLETE", "HTTP request expansion budget exceeded");
|
|
250
|
+
const url = render(resolveExpression(call.arguments[0] ?? UNKNOWN, context));
|
|
251
|
+
const method = methodOf(resolveExpression(call.arguments[1] ?? UNDEFINED, context));
|
|
252
|
+
const normalized = normalizeRequest(url);
|
|
253
|
+
result.push({
|
|
254
|
+
source,
|
|
255
|
+
method,
|
|
256
|
+
...normalized,
|
|
257
|
+
...(method === null && normalized.reason === undefined
|
|
258
|
+
? { reason: "HTTP method is not statically known" }
|
|
259
|
+
: {}),
|
|
260
|
+
provenance: [...chain, provenance(file, call.range, "transport")],
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
const expand = (fn, args, source, chain, seen) => {
|
|
264
|
+
if (seen.has(fn.scope) || seen.size >= 8) {
|
|
265
|
+
result.push({
|
|
266
|
+
source,
|
|
267
|
+
method: null,
|
|
268
|
+
path: null,
|
|
269
|
+
origin: null,
|
|
270
|
+
reason: "local wrapper recursion/depth budget exceeded",
|
|
271
|
+
provenance: chain,
|
|
272
|
+
});
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
instantiated.add(fn.scope);
|
|
276
|
+
const env = new Map(fn.parameters.map((param, index) => [
|
|
277
|
+
param.name,
|
|
278
|
+
args[index] ?? param.defaultValue ?? UNDEFINED,
|
|
279
|
+
]));
|
|
280
|
+
const context = { file, scope: fn.scope, env };
|
|
281
|
+
for (const call of file.calls.filter((call) => call.scope === fn.scope)) {
|
|
282
|
+
if (transportName(file, call))
|
|
283
|
+
emit(call, context, source, chain);
|
|
284
|
+
else {
|
|
285
|
+
const target = localFunction(file, call);
|
|
286
|
+
if (target !== null && wrappers.has(target.scope))
|
|
287
|
+
expand(target, call.arguments.map((arg) => resolveExpression(arg, context)), source, [...chain, provenance(file, call.range, "transport", target.name)], new Set([...seen, fn.scope]));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
const instantiated = new Set();
|
|
292
|
+
for (const call of file.calls) {
|
|
293
|
+
const target = localFunction(file, call);
|
|
294
|
+
if (target === null || !wrappers.has(target.scope) || target.parameters.length === 0)
|
|
295
|
+
continue;
|
|
296
|
+
const args = call.arguments.map((arg) => resolveExpression(arg, { file, scope: call.scope, env: new Map() }));
|
|
297
|
+
// Pure forwarding calls are expanded through their concrete callers, not invented as requests.
|
|
298
|
+
if (args.length === 0 ||
|
|
299
|
+
args.every((arg) => arg.kind === "name" || arg.kind === "unknown" || arg.kind === "undefined"))
|
|
300
|
+
continue;
|
|
301
|
+
instantiated.add(target.scope);
|
|
302
|
+
expand(target, args, provenance(file, call.range, "request", target.name), [], new Set());
|
|
303
|
+
}
|
|
304
|
+
for (const call of transportCalls) {
|
|
305
|
+
if (instantiated.has(call.scope))
|
|
306
|
+
continue;
|
|
307
|
+
emit(call, { file, scope: call.scope, env: new Map() }, provenance(file, call.range, "request"), []);
|
|
308
|
+
}
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
//# sourceMappingURL=requests.js.map
|