@ploaness/governance 1.0.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 +21 -0
- package/README.md +13 -0
- package/dist/access-boundary.d.ts +41 -0
- package/dist/access-boundary.d.ts.map +1 -0
- package/dist/access-boundary.js +26 -0
- package/dist/access-boundary.js.map +1 -0
- package/dist/agent-references.d.ts +12 -0
- package/dist/agent-references.d.ts.map +1 -0
- package/dist/agent-references.js +53 -0
- package/dist/agent-references.js.map +1 -0
- package/dist/architecture-layers.d.ts +11 -0
- package/dist/architecture-layers.d.ts.map +1 -0
- package/dist/architecture-layers.js +47 -0
- package/dist/architecture-layers.js.map +1 -0
- package/dist/asset-policy.d.ts +138 -0
- package/dist/asset-policy.d.ts.map +1 -0
- package/dist/asset-policy.js +312 -0
- package/dist/asset-policy.js.map +1 -0
- package/dist/banned-typography.d.ts +13 -0
- package/dist/banned-typography.d.ts.map +1 -0
- package/dist/banned-typography.js +78 -0
- package/dist/banned-typography.js.map +1 -0
- package/dist/bundle-budget.d.ts +16 -0
- package/dist/bundle-budget.d.ts.map +1 -0
- package/dist/bundle-budget.js +37 -0
- package/dist/bundle-budget.js.map +1 -0
- package/dist/commit-message.d.ts +37 -0
- package/dist/commit-message.d.ts.map +1 -0
- package/dist/commit-message.js +144 -0
- package/dist/commit-message.js.map +1 -0
- package/dist/config-references.d.ts +17 -0
- package/dist/config-references.d.ts.map +1 -0
- package/dist/config-references.js +54 -0
- package/dist/config-references.js.map +1 -0
- package/dist/container-failure.d.ts +40 -0
- package/dist/container-failure.d.ts.map +1 -0
- package/dist/container-failure.js +154 -0
- package/dist/container-failure.js.map +1 -0
- package/dist/container-files.d.ts +36 -0
- package/dist/container-files.d.ts.map +1 -0
- package/dist/container-files.js +74 -0
- package/dist/container-files.js.map +1 -0
- package/dist/container-freshness.d.ts +102 -0
- package/dist/container-freshness.d.ts.map +1 -0
- package/dist/container-freshness.js +129 -0
- package/dist/container-freshness.js.map +1 -0
- package/dist/dependency-freshness.d.ts +129 -0
- package/dist/dependency-freshness.d.ts.map +1 -0
- package/dist/dependency-freshness.js +161 -0
- package/dist/dependency-freshness.js.map +1 -0
- package/dist/doc-blocks.d.ts +13 -0
- package/dist/doc-blocks.d.ts.map +1 -0
- package/dist/doc-blocks.js +75 -0
- package/dist/doc-blocks.js.map +1 -0
- package/dist/document-references.d.ts +44 -0
- package/dist/document-references.d.ts.map +1 -0
- package/dist/document-references.js +94 -0
- package/dist/document-references.js.map +1 -0
- package/dist/editorconfig-policy.d.ts +24 -0
- package/dist/editorconfig-policy.d.ts.map +1 -0
- package/dist/editorconfig-policy.js +106 -0
- package/dist/editorconfig-policy.js.map +1 -0
- package/dist/environment-files.d.ts +42 -0
- package/dist/environment-files.d.ts.map +1 -0
- package/dist/environment-files.js +76 -0
- package/dist/environment-files.js.map +1 -0
- package/dist/file-roles.d.ts +39 -0
- package/dist/file-roles.d.ts.map +1 -0
- package/dist/file-roles.js +82 -0
- package/dist/file-roles.js.map +1 -0
- package/dist/generated-denial.d.ts +42 -0
- package/dist/generated-denial.d.ts.map +1 -0
- package/dist/generated-denial.js +87 -0
- package/dist/generated-denial.js.map +1 -0
- package/dist/history-policy.d.ts +32 -0
- package/dist/history-policy.d.ts.map +1 -0
- package/dist/history-policy.js +50 -0
- package/dist/history-policy.js.map +1 -0
- package/dist/image-assets.d.ts +14 -0
- package/dist/image-assets.d.ts.map +1 -0
- package/dist/image-assets.js +205 -0
- package/dist/image-assets.js.map +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/install-policy.d.ts +35 -0
- package/dist/install-policy.d.ts.map +1 -0
- package/dist/install-policy.js +61 -0
- package/dist/install-policy.js.map +1 -0
- package/dist/json-shapes.d.ts +86 -0
- package/dist/json-shapes.d.ts.map +1 -0
- package/dist/json-shapes.js +124 -0
- package/dist/json-shapes.js.map +1 -0
- package/dist/license-allowlist.d.ts +15 -0
- package/dist/license-allowlist.d.ts.map +1 -0
- package/dist/license-allowlist.js +157 -0
- package/dist/license-allowlist.js.map +1 -0
- package/dist/network-policy.d.ts +69 -0
- package/dist/network-policy.d.ts.map +1 -0
- package/dist/network-policy.js +173 -0
- package/dist/network-policy.js.map +1 -0
- package/dist/paired-assets.d.ts +34 -0
- package/dist/paired-assets.d.ts.map +1 -0
- package/dist/paired-assets.js +63 -0
- package/dist/paired-assets.js.map +1 -0
- package/dist/payload-access.d.ts +9 -0
- package/dist/payload-access.d.ts.map +1 -0
- package/dist/payload-access.js +168 -0
- package/dist/payload-access.js.map +1 -0
- package/dist/payload-policy.d.ts +18 -0
- package/dist/payload-policy.d.ts.map +1 -0
- package/dist/payload-policy.js +124 -0
- package/dist/payload-policy.js.map +1 -0
- package/dist/payload-source.d.ts +29 -0
- package/dist/payload-source.d.ts.map +1 -0
- package/dist/payload-source.js +84 -0
- package/dist/payload-source.js.map +1 -0
- package/dist/release-age.d.ts +23 -0
- package/dist/release-age.d.ts.map +1 -0
- package/dist/release-age.js +38 -0
- package/dist/release-age.js.map +1 -0
- package/dist/request-limits.d.ts +39 -0
- package/dist/request-limits.d.ts.map +1 -0
- package/dist/request-limits.js +57 -0
- package/dist/request-limits.js.map +1 -0
- package/dist/run-plan.d.ts +32 -0
- package/dist/run-plan.d.ts.map +1 -0
- package/dist/run-plan.js +43 -0
- package/dist/run-plan.js.map +1 -0
- package/dist/run-policy.d.ts +32 -0
- package/dist/run-policy.d.ts.map +1 -0
- package/dist/run-policy.js +37 -0
- package/dist/run-policy.js.map +1 -0
- package/dist/secret-policy.d.ts +13 -0
- package/dist/secret-policy.d.ts.map +1 -0
- package/dist/secret-policy.js +69 -0
- package/dist/secret-policy.js.map +1 -0
- package/dist/settings-layering.d.ts +45 -0
- package/dist/settings-layering.d.ts.map +1 -0
- package/dist/settings-layering.js +134 -0
- package/dist/settings-layering.js.map +1 -0
- package/dist/settings.d.ts +238 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +378 -0
- package/dist/settings.js.map +1 -0
- package/dist/skill-manifest.d.ts +18 -0
- package/dist/skill-manifest.d.ts.map +1 -0
- package/dist/skill-manifest.js +125 -0
- package/dist/skill-manifest.js.map +1 -0
- package/dist/source-text.d.ts +83 -0
- package/dist/source-text.d.ts.map +1 -0
- package/dist/source-text.js +310 -0
- package/dist/source-text.js.map +1 -0
- package/dist/suppression-ceiling.d.ts +48 -0
- package/dist/suppression-ceiling.d.ts.map +1 -0
- package/dist/suppression-ceiling.js +115 -0
- package/dist/suppression-ceiling.js.map +1 -0
- package/dist/tailwind-tokens.d.ts +16 -0
- package/dist/tailwind-tokens.d.ts.map +1 -0
- package/dist/tailwind-tokens.js +70 -0
- package/dist/tailwind-tokens.js.map +1 -0
- package/dist/text-escapes.d.ts +7 -0
- package/dist/text-escapes.d.ts.map +1 -0
- package/dist/text-escapes.js +14 -0
- package/dist/text-escapes.js.map +1 -0
- package/dist/toolchain-pins.d.ts +42 -0
- package/dist/toolchain-pins.d.ts.map +1 -0
- package/dist/toolchain-pins.js +99 -0
- package/dist/toolchain-pins.js.map +1 -0
- package/dist/url-safety.d.ts +12 -0
- package/dist/url-safety.d.ts.map +1 -0
- package/dist/url-safety.js +58 -0
- package/dist/url-safety.js.map +1 -0
- package/dist/version-policy.d.ts +99 -0
- package/dist/version-policy.d.ts.map +1 -0
- package/dist/version-policy.js +306 -0
- package/dist/version-policy.js.map +1 -0
- package/dist/vulnerability-policy.d.ts +66 -0
- package/dist/vulnerability-policy.d.ts.map +1 -0
- package/dist/vulnerability-policy.js +106 -0
- package/dist/vulnerability-policy.js.map +1 -0
- package/dist/wiring-policy.d.ts +226 -0
- package/dist/wiring-policy.d.ts.map +1 -0
- package/dist/wiring-policy.js +458 -0
- package/dist/wiring-policy.js.map +1 -0
- package/dist/wiring-violation.d.ts +6 -0
- package/dist/wiring-violation.d.ts.map +1 -0
- package/dist/wiring-violation.js +8 -0
- package/dist/wiring-violation.js.map +1 -0
- package/dist/workspace-policy.d.ts +177 -0
- package/dist/workspace-policy.d.ts.map +1 -0
- package/dist/workspace-policy.js +228 -0
- package/dist/workspace-policy.js.map +1 -0
- package/dist/yaml-blocks.d.ts +43 -0
- package/dist/yaml-blocks.d.ts.map +1 -0
- package/dist/yaml-blocks.js +87 -0
- package/dist/yaml-blocks.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// The determinism rule that a test reaches no network beyond the machine it runs on, expressed as a
|
|
2
|
+
// decision rather than as prose in the agent guide.
|
|
3
|
+
//
|
|
4
|
+
// The rule was checkable all along: a test that leaves the machine has to go through a socket, a
|
|
5
|
+
// resolver, or fetch, and each of those hands the destination to the runtime before anything is sent.
|
|
6
|
+
// What was missing was somewhere to put the verdict. This module is that place, and it performs no I/O -
|
|
7
|
+
// the caller reads `os.networkInterfaces()` and intercepts the entry points, then asks here.
|
|
8
|
+
//
|
|
9
|
+
// The decision is host-shaped rather than allowlist-shaped on purpose. There is no setting that widens
|
|
10
|
+
// it, because "the database this suite needs happens to live elsewhere" is the exact case the rule
|
|
11
|
+
// exists to reject: a service that exists only outside the machine is exercised through a local
|
|
12
|
+
// component that implements its protocol.
|
|
13
|
+
import { isRecord } from './json-shapes.js';
|
|
14
|
+
/** The sentence of the governing standard this module decides, quoted back in every refusal. */
|
|
15
|
+
export const NETWORK_RULE = 'A test reaches no network beyond the machine it runs on.';
|
|
16
|
+
/** The resolver entry points, every one of which queries a nameserver rather than reading a hosts file. */
|
|
17
|
+
export const RESOLVER_METHODS = [
|
|
18
|
+
'resolve',
|
|
19
|
+
'resolve4',
|
|
20
|
+
'resolve6',
|
|
21
|
+
'resolveAny',
|
|
22
|
+
'resolveCaa',
|
|
23
|
+
'resolveCname',
|
|
24
|
+
'resolveMx',
|
|
25
|
+
'resolveNaptr',
|
|
26
|
+
'resolveNs',
|
|
27
|
+
'resolvePtr',
|
|
28
|
+
'resolveSoa',
|
|
29
|
+
'resolveSrv',
|
|
30
|
+
'resolveTxt',
|
|
31
|
+
'reverse',
|
|
32
|
+
];
|
|
33
|
+
// The prefix an IPv4 address wears when it is carried inside an IPv6 one.
|
|
34
|
+
const IPV4_MAPPED_PREFIX = '::ffff:';
|
|
35
|
+
const IPV4_LOOPBACK = /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
|
|
36
|
+
const ZONE_SEPARATOR = '%';
|
|
37
|
+
const OPENING_BRACKET = '[';
|
|
38
|
+
const CLOSING_BRACKET = ']';
|
|
39
|
+
const DOT = '.';
|
|
40
|
+
// `localhost` and, per RFC 6761, every name beneath it. Written as two string tests rather than as one
|
|
41
|
+
// alternation, because the pattern that expresses it backtracks super-linearly on a long name.
|
|
42
|
+
const LOCALHOST = 'localhost';
|
|
43
|
+
const isLocalhostName = (host) => host === LOCALHOST || host.endsWith(`.${LOCALHOST}`);
|
|
44
|
+
// The unspecified address and the two spellings of the IPv6 loopback. Both name this machine.
|
|
45
|
+
const LOOPBACK_LITERALS = new Set([
|
|
46
|
+
'::1',
|
|
47
|
+
'0:0:0:0:0:0:0:1',
|
|
48
|
+
'0.0.0.0',
|
|
49
|
+
'::',
|
|
50
|
+
'0:0:0:0:0:0:0:0',
|
|
51
|
+
]);
|
|
52
|
+
// One canonical spelling for a host, so an address read off this machine's interfaces and a host a test
|
|
53
|
+
// wrote compare as equals: lower case, no brackets around an IPv6 literal, no zone identifier, no
|
|
54
|
+
// trailing dot, and an IPv4-mapped IPv6 address reduced to the IPv4 address it carries.
|
|
55
|
+
// Written with string operations rather than with patterns: every pattern that expresses the same three
|
|
56
|
+
// trims backtracks super-linearly on a long host, and a host is attacker-shaped input in a suite that
|
|
57
|
+
// reads a URL out of a fixture.
|
|
58
|
+
const canonicalHost = (host) => {
|
|
59
|
+
const lowered = host
|
|
60
|
+
.toLowerCase()
|
|
61
|
+
.replaceAll(OPENING_BRACKET, '')
|
|
62
|
+
.replaceAll(CLOSING_BRACKET, '');
|
|
63
|
+
const withoutZone = lowered.split(ZONE_SEPARATOR)[0] ?? '';
|
|
64
|
+
const rooted = withoutZone.endsWith(DOT) ? withoutZone.slice(0, -1) : withoutZone;
|
|
65
|
+
return rooted.startsWith(IPV4_MAPPED_PREFIX) ? rooted.slice(IPV4_MAPPED_PREFIX.length) : rooted;
|
|
66
|
+
};
|
|
67
|
+
const isLoopback = (host) => IPV4_LOOPBACK.test(host) || LOOPBACK_LITERALS.has(host) || isLocalhostName(host);
|
|
68
|
+
/**
|
|
69
|
+
* The addresses that name this machine, read out of an `os.networkInterfaces()` result.
|
|
70
|
+
*
|
|
71
|
+
* A test that connects to the box's own routable address is still on the machine it runs on, so those
|
|
72
|
+
* addresses join the loopback range rather than being refused with everything else.
|
|
73
|
+
* @param interfaces the interface map, as `os.networkInterfaces()` returns it.
|
|
74
|
+
* @returns every address it carries, in the canonical spelling {@link isMachineLocalHost} compares against.
|
|
75
|
+
*/
|
|
76
|
+
export const localAddresses = (interfaces) => new Set(Object.values(interfaces)
|
|
77
|
+
.flatMap((entries) => entries ?? [])
|
|
78
|
+
.map((entry) => canonicalHost(entry.address)));
|
|
79
|
+
/**
|
|
80
|
+
* Whether a host names the machine the test runs on.
|
|
81
|
+
*
|
|
82
|
+
* A name that is not one of the localhost spellings is refused on its face rather than resolved, because
|
|
83
|
+
* a resolution is itself the thing being refused. That is why a bare service name such as `postgres`,
|
|
84
|
+
* and a docker gateway such as `host.docker.internal`, do not pass: neither can be shown to be this
|
|
85
|
+
* machine without asking a nameserver.
|
|
86
|
+
* @param host the host as the caller wrote it, or undefined when the caller named none.
|
|
87
|
+
* @param local the addresses this machine carries, from {@link localAddresses}.
|
|
88
|
+
* @returns true when the reach stays on this machine.
|
|
89
|
+
*/
|
|
90
|
+
export const isMachineLocalHost = (host, local) => {
|
|
91
|
+
if (host === undefined || host.length === 0) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
const canonical = canonicalHost(host);
|
|
95
|
+
return isLoopback(canonical) || local.has(canonical);
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* The host a URL names.
|
|
99
|
+
*
|
|
100
|
+
* Undefined when the value names no host - a relative path, which under a DOM environment is the page's
|
|
101
|
+
* own origin and reaches nothing, or a string that is not a URL at all.
|
|
102
|
+
* @param rawUrl the value a test passed to fetch.
|
|
103
|
+
* @returns the host, or undefined.
|
|
104
|
+
*/
|
|
105
|
+
export const hostOfUrl = (rawUrl) => {
|
|
106
|
+
try {
|
|
107
|
+
return new URL(rawUrl).hostname;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
// A string first argument is a pipe or socket path rather than a port, which is the same reading Node
|
|
114
|
+
// itself applies: anything that is not a number names a path.
|
|
115
|
+
const isPipeName = (value) => Number.isNaN(Number(value));
|
|
116
|
+
const describePort = (port) => typeof port === 'number' || typeof port === 'string' ? String(port) : '';
|
|
117
|
+
const fromOptions = (options) => {
|
|
118
|
+
const socketPath = options['path'];
|
|
119
|
+
if (typeof socketPath === 'string') {
|
|
120
|
+
return { api: 'connect', host: undefined, destination: socketPath };
|
|
121
|
+
}
|
|
122
|
+
const declaredHost = options['host'];
|
|
123
|
+
const host = typeof declaredHost === 'string' ? declaredHost : undefined;
|
|
124
|
+
return {
|
|
125
|
+
api: 'connect',
|
|
126
|
+
host,
|
|
127
|
+
destination: `${host ?? LOCALHOST}:${describePort(options['port'])}`,
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Read one `net.Socket.prototype.connect` call, whichever of its three forms the caller used.
|
|
132
|
+
*
|
|
133
|
+
* A unix socket reports no host, because a path is not a network: the two ends are the same machine by
|
|
134
|
+
* construction, and refusing one would break every test that talks to a local service over a socket file.
|
|
135
|
+
* @param callArguments the arguments the call was made with.
|
|
136
|
+
* @returns the attempt, normalised.
|
|
137
|
+
*/
|
|
138
|
+
export const describeSocketTarget = (callArguments) => {
|
|
139
|
+
const [first, second] = callArguments;
|
|
140
|
+
if (isRecord(first)) {
|
|
141
|
+
return fromOptions(first);
|
|
142
|
+
}
|
|
143
|
+
if (typeof first === 'string' && isPipeName(first)) {
|
|
144
|
+
return { api: 'connect', host: undefined, destination: first };
|
|
145
|
+
}
|
|
146
|
+
const host = typeof second === 'string' ? second : undefined;
|
|
147
|
+
return {
|
|
148
|
+
api: 'connect',
|
|
149
|
+
host,
|
|
150
|
+
destination: `${host ?? LOCALHOST}:${describePort(first)}`,
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
const OFF_MACHINE_REASON = 'that host is not this machine';
|
|
154
|
+
const RESOLVER_REASON = 'a resolver query is a packet to a nameserver, which no hosts file answers';
|
|
155
|
+
const refusal = (attempt, reason) => `${NETWORK_RULE} A test reached ${attempt.destination} through ${attempt.api}, and ${reason}. ` +
|
|
156
|
+
'Run a real component on this machine and point the test at it.';
|
|
157
|
+
/**
|
|
158
|
+
* Decide one attempted reach.
|
|
159
|
+
*
|
|
160
|
+
* A resolver query is refused whatever host it names, loopback included. The resolver family bypasses
|
|
161
|
+
* the hosts file and sends a packet to a nameserver, so `resolve4('localhost')` leaves the machine as
|
|
162
|
+
* surely as any other name does.
|
|
163
|
+
* @param attempt the reach to judge.
|
|
164
|
+
* @param local the addresses this machine carries, from {@link localAddresses}.
|
|
165
|
+
* @returns undefined when the reach is allowed, or the message a refusal should carry.
|
|
166
|
+
*/
|
|
167
|
+
export const findNetworkEscape = (attempt, local) => {
|
|
168
|
+
if (attempt.api === 'resolve') {
|
|
169
|
+
return refusal(attempt, RESOLVER_REASON);
|
|
170
|
+
}
|
|
171
|
+
return isMachineLocalHost(attempt.host, local) ? undefined : refusal(attempt, OFF_MACHINE_REASON);
|
|
172
|
+
};
|
|
173
|
+
//# sourceMappingURL=network-policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network-policy.js","sourceRoot":"","sources":["../src/network-policy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ManagedAsset } from './asset-policy.js';
|
|
2
|
+
/** A root file and the shipped body generated from it. */
|
|
3
|
+
export interface PairedAsset {
|
|
4
|
+
/** The path in the ploaness repository, which is the editable source. */
|
|
5
|
+
readonly rootPath: string;
|
|
6
|
+
/** The generated body, relative to the assets package root. */
|
|
7
|
+
readonly assetPath: string;
|
|
8
|
+
}
|
|
9
|
+
/** The contents of both sides of one pair, read by the caller so this module stays pure. */
|
|
10
|
+
export interface PairedAssetState {
|
|
11
|
+
readonly pair: PairedAsset;
|
|
12
|
+
/** The root file's content, or undefined when the root file is missing. */
|
|
13
|
+
readonly rootContent: string | undefined;
|
|
14
|
+
/** The generated body's content, or undefined when the body is missing. */
|
|
15
|
+
readonly assetContent: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
/** A pair whose two sides no longer agree. */
|
|
18
|
+
export interface PairedAssetDrift {
|
|
19
|
+
readonly rootPath: string;
|
|
20
|
+
readonly reason: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Derive the root-to-body pairs from the manifest catalogue.
|
|
24
|
+
* @param assets the parsed manifest entries.
|
|
25
|
+
* @returns one pair per PINNED or SEED entry that ploaness generates from a root file.
|
|
26
|
+
*/
|
|
27
|
+
export declare const pairedAssets: (assets: readonly ManagedAsset[]) => readonly PairedAsset[];
|
|
28
|
+
/**
|
|
29
|
+
* Report every pair whose generated body has drifted from its root file.
|
|
30
|
+
* @param states both sides of every pair, already read.
|
|
31
|
+
* @returns one drift per disagreeing pair; empty means every consumer receives what this repository holds.
|
|
32
|
+
*/
|
|
33
|
+
export declare const findPairedAssetDrift: (states: readonly PairedAssetState[]) => readonly PairedAssetDrift[];
|
|
34
|
+
//# sourceMappingURL=paired-assets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paired-assets.d.ts","sourceRoot":"","sources":["../src/paired-assets.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAiCrD,0DAA0D;AAC1D,MAAM,WAAW,WAAW;IAC1B,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,4FAA4F;AAC5F,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAA;IACxC,2EAA2E;IAC3E,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;CAC1C;AAED,8CAA8C;AAC9C,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,SAAS,YAAY,EAAE,KAAG,SAAS,WAAW,EAY9E,CAAA;AAkBL;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,SAAS,gBAAgB,EAAE,KAClC,SAAS,gBAAgB,EAIxB,CAAA"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Where an `.asset` body lives inside the assets package, relative to that package's root. */
|
|
2
|
+
const ASSET_DIRECTORY = 'files';
|
|
3
|
+
/** The suffix npm forces on a packed body: a packed `.npmrc` is stripped and a `.gitignore` renamed. */
|
|
4
|
+
const ASSET_SUFFIX = '.asset';
|
|
5
|
+
// Some paths are authored where they are shipped rather than copied from a root file, all because
|
|
6
|
+
// ploaness is not a consumer of itself.
|
|
7
|
+
//
|
|
8
|
+
// `.ploaness/agent-guide.md` maps the guideline onto the harness for a consumer, and ploaness has no
|
|
9
|
+
// `.ploaness/` directory. `.dockerignore` bounds what a Docker build sends to the daemon, and ploaness
|
|
10
|
+
// builds no image, so a copy at this root would be a file no tool here reads. The managed sweeps under
|
|
11
|
+
// `tests/e2e` drive a Payload application, and ploaness is a library with no application to drive;
|
|
12
|
+
// pairing one with a root file would mean keeping a Next.js project in this repository for a spec to
|
|
13
|
+
// compile against. What proves those bodies instead is the third verification leg AGENTS.md already
|
|
14
|
+
// requires: a real consumer runs them.
|
|
15
|
+
//
|
|
16
|
+
// `src/proxy.ts` is the security-header baseline, and it is authored here for the same reason as the
|
|
17
|
+
// sweeps: it is Next middleware, and there is no Next application in this repository for a root copy of
|
|
18
|
+
// it to compile against.
|
|
19
|
+
//
|
|
20
|
+
// A SECTION entry is likewise unpaired: its body is the managed block, not a copy of any whole file.
|
|
21
|
+
const ASSET_AUTHORED_PATHS = new Set([
|
|
22
|
+
'.ploaness/agent-guide.md',
|
|
23
|
+
'.dockerignore',
|
|
24
|
+
'tests/e2e/a11y.e2e.spec.ts',
|
|
25
|
+
'tests/e2e/security-headers.e2e.spec.ts',
|
|
26
|
+
'tests/e2e/access-boundary.e2e.spec.ts',
|
|
27
|
+
'src/proxy.ts',
|
|
28
|
+
]);
|
|
29
|
+
/**
|
|
30
|
+
* Derive the root-to-body pairs from the manifest catalogue.
|
|
31
|
+
* @param assets the parsed manifest entries.
|
|
32
|
+
* @returns one pair per PINNED or SEED entry that ploaness generates from a root file.
|
|
33
|
+
*/
|
|
34
|
+
export const pairedAssets = (assets) => assets
|
|
35
|
+
.filter((asset) => (asset.disposition === 'PINNED' || asset.disposition === 'SEED') &&
|
|
36
|
+
!ASSET_AUTHORED_PATHS.has(asset.path))
|
|
37
|
+
.map((asset) => ({
|
|
38
|
+
rootPath: asset.path,
|
|
39
|
+
assetPath: `${ASSET_DIRECTORY}/${asset.path}${ASSET_SUFFIX}`,
|
|
40
|
+
}));
|
|
41
|
+
const driftReason = (state) => {
|
|
42
|
+
if (state.rootContent === undefined) {
|
|
43
|
+
return (`no root file to generate ${state.pair.assetPath} from; add it, or declare ` +
|
|
44
|
+
'the path as authored directly as an asset');
|
|
45
|
+
}
|
|
46
|
+
if (state.assetContent === undefined) {
|
|
47
|
+
return `${state.pair.assetPath} is missing; regenerate it with \`pnpm run build\``;
|
|
48
|
+
}
|
|
49
|
+
return state.rootContent === state.assetContent
|
|
50
|
+
? undefined
|
|
51
|
+
: `${state.pair.assetPath} no longer matches ${state.pair.rootPath}; consumers would ` +
|
|
52
|
+
'receive stale content. Regenerate it with `pnpm run build`';
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Report every pair whose generated body has drifted from its root file.
|
|
56
|
+
* @param states both sides of every pair, already read.
|
|
57
|
+
* @returns one drift per disagreeing pair; empty means every consumer receives what this repository holds.
|
|
58
|
+
*/
|
|
59
|
+
export const findPairedAssetDrift = (states) => states.flatMap((state) => {
|
|
60
|
+
const reason = driftReason(state);
|
|
61
|
+
return reason === undefined ? [] : [{ rootPath: state.pair.rootPath, reason }];
|
|
62
|
+
});
|
|
63
|
+
//# sourceMappingURL=paired-assets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paired-assets.js","sourceRoot":"","sources":["../src/paired-assets.ts"],"names":[],"mappings":"AAaA,+FAA+F;AAC/F,MAAM,eAAe,GAAW,OAAO,CAAA;AAEvC,wGAAwG;AACxG,MAAM,YAAY,GAAW,QAAQ,CAAA;AAErC,kGAAkG;AAClG,wCAAwC;AACxC,EAAE;AACF,qGAAqG;AACrG,uGAAuG;AACvG,uGAAuG;AACvG,mGAAmG;AACnG,qGAAqG;AACrG,oGAAoG;AACpG,uCAAuC;AACvC,EAAE;AACF,qGAAqG;AACrG,wGAAwG;AACxG,yBAAyB;AACzB,EAAE;AACF,qGAAqG;AACrG,MAAM,oBAAoB,GAAwB,IAAI,GAAG,CAAS;IAChE,0BAA0B;IAC1B,eAAe;IACf,4BAA4B;IAC5B,wCAAwC;IACxC,uCAAuC;IACvC,cAAc;CACf,CAAC,CAAA;AAyBF;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAA+B,EAA0B,EAAE,CACtF,MAAM;KACH,MAAM,CACL,CAAC,KAAmB,EAAW,EAAE,CAC/B,CAAC,KAAK,CAAC,WAAW,KAAK,QAAQ,IAAI,KAAK,CAAC,WAAW,KAAK,MAAM,CAAC;IAChE,CAAC,oBAAoB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CACxC;KACA,GAAG,CACF,CAAC,KAAmB,EAAe,EAAE,CAAC,CAAC;IACrC,QAAQ,EAAE,KAAK,CAAC,IAAI;IACpB,SAAS,EAAE,GAAG,eAAe,IAAI,KAAK,CAAC,IAAI,GAAG,YAAY,EAAE;CAC7D,CAAC,CACH,CAAA;AAEL,MAAM,WAAW,GAAG,CAAC,KAAuB,EAAsB,EAAE;IAClE,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACpC,OAAO,CACL,4BAA4B,KAAK,CAAC,IAAI,CAAC,SAAS,4BAA4B;YAC5E,2CAA2C,CAC5C,CAAA;IACH,CAAC;IACD,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACrC,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,SAAS,oDAAoD,CAAA;IACpF,CAAC;IACD,OAAO,KAAK,CAAC,WAAW,KAAK,KAAK,CAAC,YAAY;QAC7C,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,SAAS,sBAAsB,KAAK,CAAC,IAAI,CAAC,QAAQ,oBAAoB;YAClF,4DAA4D,CAAA;AACpE,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,MAAmC,EACN,EAAE,CAC/B,MAAM,CAAC,OAAO,CAAC,CAAC,KAAuB,EAA+B,EAAE;IACtE,MAAM,MAAM,GAAuB,WAAW,CAAC,KAAK,CAAC,CAAA;IACrD,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAA;AAChF,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type PayloadViolation } from './payload-source.js';
|
|
2
|
+
export declare const findUndeclaredAccess: (source: string) => readonly PayloadViolation[];
|
|
3
|
+
/** Report an auth collection that leaves the login-attempt cap and lock time to Payload's defaults. */
|
|
4
|
+
export declare const findUnhardenedAuth: (source: string) => readonly PayloadViolation[];
|
|
5
|
+
/** Report a config whose drafts are readable by an unauthenticated client. */
|
|
6
|
+
export declare const findAnonymousDraftReads: (source: string) => readonly PayloadViolation[];
|
|
7
|
+
/** Report an upload collection that accepts whatever file type Payload's defaults allow. */
|
|
8
|
+
export declare const findUnrestrictedUploads: (source: string) => readonly PayloadViolation[];
|
|
9
|
+
//# sourceMappingURL=payload-access.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payload-access.d.ts","sourceRoot":"","sources":["../src/payload-access.ts"],"names":[],"mappings":"AAOA,OAAO,EAIL,KAAK,gBAAgB,EACtB,MAAM,qBAAqB,CAAA;AA4E5B,eAAO,MAAM,oBAAoB,GAAI,QAAQ,MAAM,KAAG,SAAS,gBAAgB,EAkB3E,CAAA;AAkCJ,uGAAuG;AACvG,eAAO,MAAM,kBAAkB,GAAI,QAAQ,MAAM,KAAG,SAAS,gBAAgB,EAG1E,CAAA;AA2CH,8EAA8E;AAC9E,eAAO,MAAM,uBAAuB,GAAI,QAAQ,MAAM,KAAG,SAAS,gBAAgB,EAa/E,CAAA;AA6BH,4FAA4F;AAC5F,eAAO,MAAM,uBAAuB,GAAI,QAAQ,MAAM,KAAG,SAAS,gBAAgB,EAG/E,CAAA"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// The access-control half of the Payload rules: who a collection or a global lets in, and what it
|
|
2
|
+
// leaves to the framework's defaults.
|
|
3
|
+
//
|
|
4
|
+
// These rules are separated from the Local API rules in `payload-policy.ts` because they share a
|
|
5
|
+
// question the others do not ask - "which object literal IS this config" - and because together the two
|
|
6
|
+
// halves passed the file size cap. Every one of them is a security decision that Payload will silently
|
|
7
|
+
// make on the project's behalf if the project does not make it first.
|
|
8
|
+
import { configBody, depthOneBlockKeys, depthOneValue, } from './payload-source.js';
|
|
9
|
+
import { lineOf, NOT_FOUND, topLevelKeys } from './source-text.js';
|
|
10
|
+
// The three ways a Payload config is declared, all of them idiomatic:
|
|
11
|
+
//
|
|
12
|
+
// const Users: CollectionConfig = { ... } the annotation
|
|
13
|
+
// const Users: CollectionConfig<'users'> = { ... } the annotation, with the slug as a type argument
|
|
14
|
+
// const Users = { ... } satisfies CollectionConfig the inferred form
|
|
15
|
+
//
|
|
16
|
+
// Only the first was matched for a long time, because the lookahead that stopped `CollectionConfigs`
|
|
17
|
+
// from matching also excluded `<`, and nothing looked for `satisfies` at all. A collection written
|
|
18
|
+
// either of the other two ways passed every rule below without one of them ever reading it - which is
|
|
19
|
+
// the worst failure a gate can have, because it is indistinguishable from a pass.
|
|
20
|
+
const declarationPattern = (typeName) => new RegExp(String.raw `(:|satisfies)\s*${typeName}(?=[<=,)\s]|$)`, 'g');
|
|
21
|
+
/** The operations a collection must decide for itself rather than inherit. */
|
|
22
|
+
const COLLECTION_OPERATIONS = ['create', 'read', 'update', 'delete'];
|
|
23
|
+
/** A global has no create or delete: it exists from the moment it is configured. */
|
|
24
|
+
const GLOBAL_OPERATIONS = ['read', 'update'];
|
|
25
|
+
const CONFIG_KINDS = [
|
|
26
|
+
{
|
|
27
|
+
label: 'CollectionConfig',
|
|
28
|
+
declaration: declarationPattern('CollectionConfig'),
|
|
29
|
+
operations: COLLECTION_OPERATIONS,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
label: 'GlobalConfig',
|
|
33
|
+
declaration: declarationPattern('GlobalConfig'),
|
|
34
|
+
operations: GLOBAL_OPERATIONS,
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
const SATISFIES = 'satisfies';
|
|
38
|
+
// EVERY config in the file, not the first. `search` returned one offset, so a module exporting two
|
|
39
|
+
// collections - or a collection beside a global - had its second config judged by nothing at all. The
|
|
40
|
+
// Local API rules already iterated their matches; these did not, and the asymmetry was invisible
|
|
41
|
+
// because a file with one config, which is most of them, behaves identically either way.
|
|
42
|
+
const findConfigs = (source, kind) => [...source.matchAll(kind.declaration)].flatMap((match) => {
|
|
43
|
+
const body = configBody(source, match.index, match[1] === SATISFIES);
|
|
44
|
+
return body === undefined ? [] : [{ marker: match.index, body }];
|
|
45
|
+
});
|
|
46
|
+
const eachConfig = (source, judge) => CONFIG_KINDS.flatMap((kind) => findConfigs(source, kind).flatMap((found) => judge(kind, found)));
|
|
47
|
+
// Payload fills the missing operations in during sanitisation, so a partial access block is invisible
|
|
48
|
+
// the moment the app boots - and its default for a non-auth collection is public read. Checking that
|
|
49
|
+
// the word `access` appears somewhere in the file, which is what this rule used to do, accepted a block
|
|
50
|
+
// that declared one operation out of four.
|
|
51
|
+
export const findUndeclaredAccess = (source) => eachConfig(source, (kind, found) => {
|
|
52
|
+
const declared = depthOneBlockKeys(found.body, 'access');
|
|
53
|
+
const missing = kind.operations.filter((operation) => !declared.includes(operation));
|
|
54
|
+
return missing.length === 0
|
|
55
|
+
? []
|
|
56
|
+
: [
|
|
57
|
+
{
|
|
58
|
+
line: lineOf(source, found.marker),
|
|
59
|
+
rule: 'require-complete-access',
|
|
60
|
+
reason: `a ${kind.label} must declare access for ${kind.operations.join(', ')}; ` +
|
|
61
|
+
`${missing.join(', ')} ${missing.length === 1 ? 'is' : 'are'} left to the Payload ` +
|
|
62
|
+
'defaults, which allow public read',
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
});
|
|
66
|
+
// Payload locks nothing by default: without a login-attempt cap and a lock time, an auth collection
|
|
67
|
+
// accepts guesses at whatever rate a client can make them. `auth: true` is the bare enable, so it is
|
|
68
|
+
// the case this rule most needs to catch.
|
|
69
|
+
const AUTH_HARDENING_KEYS = ['maxLoginAttempts', 'lockTime'];
|
|
70
|
+
const COLLECTION = 'CollectionConfig';
|
|
71
|
+
const unhardenedAuthIn = (source, found) => {
|
|
72
|
+
const value = depthOneValue(found.body, 'auth');
|
|
73
|
+
if (value === undefined) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const declared = value.trimStart().startsWith('{')
|
|
77
|
+
? topLevelKeys(value, 0)
|
|
78
|
+
: [];
|
|
79
|
+
const missing = AUTH_HARDENING_KEYS.filter((key) => !declared.includes(key));
|
|
80
|
+
return missing.length === 0
|
|
81
|
+
? []
|
|
82
|
+
: [
|
|
83
|
+
{
|
|
84
|
+
line: lineOf(source, found.marker),
|
|
85
|
+
rule: 'require-auth-hardening',
|
|
86
|
+
reason: `an auth collection must declare ${AUTH_HARDENING_KEYS.join(' and ')}; ` +
|
|
87
|
+
`${missing.join(' and ')} left to the Payload defaults means a login attempt is never ` +
|
|
88
|
+
'capped and an account is never locked',
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
};
|
|
92
|
+
/** Report an auth collection that leaves the login-attempt cap and lock time to Payload's defaults. */
|
|
93
|
+
export const findUnhardenedAuth = (source) => eachConfig(source, (kind, found) => kind.label === COLLECTION ? unhardenedAuthIn(source, found) : []);
|
|
94
|
+
// A draft is unpublished content. With versions.drafts enabled, `?draft=true` serves it to whoever the
|
|
95
|
+
// read rule admits - so a read that is unconditionally true publishes every draft to anyone.
|
|
96
|
+
const DRAFTS_ENABLED = /drafts\s*:\s*(?:true|\{)/;
|
|
97
|
+
// The inline always-true read.
|
|
98
|
+
//
|
|
99
|
+
// The return type is optional in the pattern and effectively mandatory in a governed project, which is
|
|
100
|
+
// the whole reason it appears here. `explicit-function-return-type` makes a conforming project write
|
|
101
|
+
// `read: (): boolean => true`, and a pattern demanding `()` immediately before `=>` matched none of
|
|
102
|
+
// those - so this rule reported nothing on the only spelling the harness permits. `[^=]*` cannot run
|
|
103
|
+
// past the arrow it precedes, which keeps the optional part from swallowing the match.
|
|
104
|
+
const ALWAYS_TRUE_READ = /read\s*:\s*\(\s*\)\s*(?::[^=]*)?=>\s*true/;
|
|
105
|
+
// Where the access block's own braces close, so the search below cannot run past them into the fields.
|
|
106
|
+
const blockEnd = (access, open) => {
|
|
107
|
+
const body = configBody(access.slice(open), 0, false);
|
|
108
|
+
return body === undefined ? access.length : open + body.length;
|
|
109
|
+
};
|
|
110
|
+
// The read is looked for inside the access block alone. `depthOneValue` returns everything from the
|
|
111
|
+
// key's colon to the end of the enclosing literal, so testing it whole meant a FIELD-level
|
|
112
|
+
// `read: () => true` - which grants nothing beyond that one field - was reported as though the
|
|
113
|
+
// collection itself were open to anyone.
|
|
114
|
+
//
|
|
115
|
+
// Only the inline form is decidable here, and the lint pass forbids that form in a config file, so a
|
|
116
|
+
// conforming project writes `read: anyone` and this rule stays silent on it. What covers the conforming
|
|
117
|
+
// case is the managed access-boundary sweep, which asks the running application what it grants rather
|
|
118
|
+
// than reading the source. This rule reaches a config declared outside the linted directories.
|
|
119
|
+
const isDraftExposed = (body) => {
|
|
120
|
+
const versions = depthOneValue(body, 'versions');
|
|
121
|
+
if (versions === undefined || !DRAFTS_ENABLED.test(versions)) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
const access = depthOneValue(body, 'access');
|
|
125
|
+
if (access === undefined) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const open = access.indexOf('{');
|
|
129
|
+
return open !== NOT_FOUND && ALWAYS_TRUE_READ.test(access.slice(0, blockEnd(access, open)));
|
|
130
|
+
};
|
|
131
|
+
/** Report a config whose drafts are readable by an unauthenticated client. */
|
|
132
|
+
export const findAnonymousDraftReads = (source) => eachConfig(source, (kind, found) => isDraftExposed(found.body)
|
|
133
|
+
? [
|
|
134
|
+
{
|
|
135
|
+
line: lineOf(source, found.marker),
|
|
136
|
+
rule: 'no-anonymous-draft-reads',
|
|
137
|
+
reason: `a ${kind.label} with drafts enabled must not grant an unconditionally true read; ` +
|
|
138
|
+
'an unauthenticated client would fetch unpublished drafts through ?draft=true',
|
|
139
|
+
},
|
|
140
|
+
]
|
|
141
|
+
: []);
|
|
142
|
+
// `mimeTypes` defaults to undefined, so an upload collection takes whatever a client sends until the
|
|
143
|
+
// project says otherwise. Served from the application's own origin, an uploaded SVG is script that runs
|
|
144
|
+
// as the site, which is why the restriction is required rather than advised. No size cap is asked for
|
|
145
|
+
// here: Payload takes that at the config root rather than on the collection.
|
|
146
|
+
const UPLOAD_RESTRICTION = 'mimeTypes';
|
|
147
|
+
const unrestrictedUploadIn = (source, found) => {
|
|
148
|
+
const value = depthOneValue(found.body, 'upload');
|
|
149
|
+
if (value === undefined) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
const declared = value.trimStart().startsWith('{')
|
|
153
|
+
? topLevelKeys(value, 0)
|
|
154
|
+
: [];
|
|
155
|
+
return declared.includes(UPLOAD_RESTRICTION)
|
|
156
|
+
? []
|
|
157
|
+
: [
|
|
158
|
+
{
|
|
159
|
+
line: lineOf(source, found.marker),
|
|
160
|
+
rule: 'require-upload-restrictions',
|
|
161
|
+
reason: `an upload collection must declare ${UPLOAD_RESTRICTION}; left undeclared it accepts ` +
|
|
162
|
+
'any file, and an SVG served from this origin is script that runs as the site',
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
};
|
|
166
|
+
/** Report an upload collection that accepts whatever file type Payload's defaults allow. */
|
|
167
|
+
export const findUnrestrictedUploads = (source) => eachConfig(source, (kind, found) => kind.label === COLLECTION ? unrestrictedUploadIn(source, found) : []);
|
|
168
|
+
//# sourceMappingURL=payload-access.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payload-access.js","sourceRoot":"","sources":["../src/payload-access.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PayloadViolation } from './payload-source.js';
|
|
2
|
+
/**
|
|
3
|
+
* The rules that are about the language rather than about Payload.
|
|
4
|
+
*
|
|
5
|
+
* Reaching for a parent-relative import instead of the path alias is a defect in any package. Held
|
|
6
|
+
* inside the Payload rule set it ran only where Payload did, so a frontend beside the CMS - the place a
|
|
7
|
+
* deep relative import is most likely, since it has no Payload to anchor on - was never checked.
|
|
8
|
+
* @param source the file's text.
|
|
9
|
+
* @returns one violation per offending line.
|
|
10
|
+
*/
|
|
11
|
+
export declare const findSourceViolations: (source: string) => readonly PayloadViolation[];
|
|
12
|
+
/**
|
|
13
|
+
* The rules that are about Payload itself, which only a Payload package can break.
|
|
14
|
+
* @param source the file's text.
|
|
15
|
+
* @returns one violation per offending line.
|
|
16
|
+
*/
|
|
17
|
+
export declare const findPayloadViolations: (source: string) => readonly PayloadViolation[];
|
|
18
|
+
//# sourceMappingURL=payload-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payload-policy.d.ts","sourceRoot":"","sources":["../src/payload-policy.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AA6H3D;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,MAAM,KAAG,SAAS,gBAAgB,EAC/B,CAAA;AAEhD;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GAAI,QAAQ,MAAM,KAAG,SAAS,gBAAgB,EAY/E,CAAA"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Payload-specific source policy: the rules that exist only because the project is a Payload CMS
|
|
2
|
+
// application, and therefore the part of ploaness that a generic JavaScript harness cannot supply.
|
|
3
|
+
//
|
|
4
|
+
// ploaness governs a language and a framework; this module is the framework half. The rules here target
|
|
5
|
+
// Payload defects that a type checker and a generic linter both miss: a Local API read that pulls an
|
|
6
|
+
// unbounded relationship graph out of the database, and an access check that is deliberately bypassed.
|
|
7
|
+
// The access-control rules - the ones about a decision left UNWRITTEN rather than written wrongly - are
|
|
8
|
+
// in `payload-access.ts`, and the source reader both halves are decided by is in `payload-source.ts`.
|
|
9
|
+
//
|
|
10
|
+
// These rules were previously five GritQL files run as Biome plugins. Biome resolves a plugin path
|
|
11
|
+
// relative to the config that declares it, which does not survive being extended from node_modules, so
|
|
12
|
+
// they are reimplemented here instead. The move also makes them unit-testable, which they were not.
|
|
13
|
+
import { findAnonymousDraftReads, findUndeclaredAccess, findUnhardenedAuth, findUnrestrictedUploads, } from './payload-access.js';
|
|
14
|
+
import { balancedArguments, lineOf, occurrences, stripComments, topLevelSlice, } from './source-text.js';
|
|
15
|
+
// The Local API reads that must declare their bound. `find` returns a page, so either `depth` or `limit`
|
|
16
|
+
// bounds it; the single-document and global reads return one document, so only `depth` applies.
|
|
17
|
+
const BOUNDED_CALLS = [
|
|
18
|
+
{
|
|
19
|
+
call: '.find(',
|
|
20
|
+
required: ['depth', 'limit'],
|
|
21
|
+
rule: 'no-unbounded-find',
|
|
22
|
+
reason: 'bound payload.find() with an explicit depth and/or limit, or it pulls an unbounded relationship graph',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
call: '.findByID(',
|
|
26
|
+
required: ['depth'],
|
|
27
|
+
rule: 'no-unbounded-findbyid',
|
|
28
|
+
reason: 'bound payload.findByID() with an explicit depth to cap relationship population',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
call: '.findGlobal(',
|
|
32
|
+
required: ['depth'],
|
|
33
|
+
rule: 'no-unbounded-findglobal',
|
|
34
|
+
reason: 'bound payload.findGlobal() with an explicit depth to cap relationship population',
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
// Only a call on an identifier that is recognisably the Payload instance is judged, so an unrelated
|
|
38
|
+
// `array.find(...)` is never touched. `req` may itself be reached through a chain - `ctx.req.payload`
|
|
39
|
+
// inside a hook is as much the Payload instance as `req.payload` - and excluding a preceding dot meant
|
|
40
|
+
// every such call went unjudged. The chain is admitted only in front of `req`, so a property named
|
|
41
|
+
// `payload` on some unrelated object is still left alone.
|
|
42
|
+
const PAYLOAD_RECEIVER = /(?:^|[^\w$.])(?:payload|(?:[\w$]+\.)*req\.payload|this\.payload)$/;
|
|
43
|
+
const declaresKey = (topLevel, key) => new RegExp(String.raw `(^|[\s,])${key}\s*:`).test(topLevel);
|
|
44
|
+
// An unterminated call, an options variable rather than a literal, or an object spread all make the
|
|
45
|
+
// bound impossible to decide statically. Staying silent there keeps the gate free of false positives;
|
|
46
|
+
// the reviewer still sees the call.
|
|
47
|
+
const unboundedCallAt = (source, rule, found) => {
|
|
48
|
+
if (!PAYLOAD_RECEIVER.test(source.slice(0, found))) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const argumentText = balancedArguments(source, found + rule.call.length - 1);
|
|
52
|
+
if (argumentText === undefined || !argumentText.includes('{') || argumentText.includes('...')) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const topLevel = topLevelSlice(argumentText);
|
|
56
|
+
if (rule.required.some((key) => declaresKey(topLevel, key))) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
return { line: lineOf(source, found), rule: rule.rule, reason: rule.reason };
|
|
60
|
+
};
|
|
61
|
+
const findUnboundedCalls = (source) => BOUNDED_CALLS.flatMap((rule) => occurrences(source, rule.call)
|
|
62
|
+
.map((found) => unboundedCallAt(source, rule, found))
|
|
63
|
+
.filter((violation) => violation !== undefined));
|
|
64
|
+
const OVERRIDE_ACCESS = /overrideAccess\s*:\s*true/g;
|
|
65
|
+
const findOverrideAccess = (source) => [...source.matchAll(OVERRIDE_ACCESS)].map((match) => ({
|
|
66
|
+
line: lineOf(source, match.index),
|
|
67
|
+
rule: 'no-override-access',
|
|
68
|
+
reason: 'overrideAccess: true bypasses Payload access control; pass req so the access rules run, or drop the override',
|
|
69
|
+
}));
|
|
70
|
+
// Anchored on the keyword that immediately precedes the specifier rather than on the statement that
|
|
71
|
+
// opens it. Requiring the whole statement to fit on one line missed a multi-line brace list, which is
|
|
72
|
+
// how a long import is normally written, and requiring `from` missed both a side-effect import and the
|
|
73
|
+
// dynamic `import('../x')` form. Comments are already blanked, so a `from` reached here is code.
|
|
74
|
+
const RELATIVE_IMPORT = /\b(?:from|import)\s*(?:\(\s*)?['"](\.\.\/[^'"]*)['"]/g;
|
|
75
|
+
// Test helpers live outside the `@/` alias root, and the Payload admin import map is generated. The
|
|
76
|
+
// exemption is anchored at a path boundary so `../helpersOfMine` is not excused by `../helpers`.
|
|
77
|
+
//
|
|
78
|
+
// The ascent repeats, because its LENGTH was never part of that reason. Anchored at exactly one `../`,
|
|
79
|
+
// the exemption reached a spec sitting directly under `tests/` and no deeper - so a suite whose
|
|
80
|
+
// directories mirror `src/`, which is the ordinary layout, could not import the helpers the exemption
|
|
81
|
+
// exists to admit. What still discriminates is the segment the climb LANDS on: `../../../helpers/x` is
|
|
82
|
+
// a helper root reached from depth, while `../../../src/helpers/x` climbed into source and is reported.
|
|
83
|
+
const RELATIVE_IMPORT_EXEMPT = /^(?:\.\.\/)+(?:helpers|importMap)(?:\/|$)/;
|
|
84
|
+
const findDeepRelativeImports = (source) => [...source.matchAll(RELATIVE_IMPORT)].flatMap((match) => {
|
|
85
|
+
const specifier = match[1] ?? '';
|
|
86
|
+
return RELATIVE_IMPORT_EXEMPT.test(specifier)
|
|
87
|
+
? []
|
|
88
|
+
: [
|
|
89
|
+
{
|
|
90
|
+
line: lineOf(source, match.index),
|
|
91
|
+
rule: 'no-deep-relative-imports',
|
|
92
|
+
reason: `use the "@/" path alias instead of the parent-relative import "${specifier}"`,
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
});
|
|
96
|
+
/**
|
|
97
|
+
* The rules that are about the language rather than about Payload.
|
|
98
|
+
*
|
|
99
|
+
* Reaching for a parent-relative import instead of the path alias is a defect in any package. Held
|
|
100
|
+
* inside the Payload rule set it ran only where Payload did, so a frontend beside the CMS - the place a
|
|
101
|
+
* deep relative import is most likely, since it has no Payload to anchor on - was never checked.
|
|
102
|
+
* @param source the file's text.
|
|
103
|
+
* @returns one violation per offending line.
|
|
104
|
+
*/
|
|
105
|
+
export const findSourceViolations = (source) => findDeepRelativeImports(stripComments(source));
|
|
106
|
+
/**
|
|
107
|
+
* The rules that are about Payload itself, which only a Payload package can break.
|
|
108
|
+
* @param source the file's text.
|
|
109
|
+
* @returns one violation per offending line.
|
|
110
|
+
*/
|
|
111
|
+
export const findPayloadViolations = (source) => {
|
|
112
|
+
// Every rule reads the code, never the prose around it. A comment that names a banned construct in
|
|
113
|
+
// order to explain why the code avoids it must not be reported as that construct.
|
|
114
|
+
const code = stripComments(source);
|
|
115
|
+
return [
|
|
116
|
+
...findUnboundedCalls(code),
|
|
117
|
+
...findOverrideAccess(code),
|
|
118
|
+
...findUndeclaredAccess(code),
|
|
119
|
+
...findUnhardenedAuth(code),
|
|
120
|
+
...findAnonymousDraftReads(code),
|
|
121
|
+
...findUnrestrictedUploads(code),
|
|
122
|
+
];
|
|
123
|
+
};
|
|
124
|
+
//# sourceMappingURL=payload-policy.js.map
|