border-customs 0.5.1 → 0.7.1
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/README.md +161 -6
- package/dist/assets/exfil-src/rules.ts +179 -0
- package/dist/assets/exfil-src/rulesPath.ts +112 -0
- package/dist/assets/exfil-src/scan.ts +146 -0
- package/dist/assets/exfil-src/severity.ts +156 -0
- package/dist/assets/gitleaks-defaults-v8.30.1.toml +50 -0
- package/dist/assets/package-lock.json +2 -2
- package/dist/index.js +1026 -116
- package/index.ts +7 -0
- package/package.json +3 -2
- package/plugin/border.ts +169 -58
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// provenance: .omo/plans/border-exfil-lens.md T1 — scan surfaces for the native
|
|
2
|
+
// exfil legs: scanTreeText (blob/tree facet) and scanMessageText (commit
|
|
3
|
+
// message facet). Findings carry facet+channel attribution resolved through
|
|
4
|
+
// severity.ts's matrix — the ONLY emitter of severities in this package; the
|
|
5
|
+
// predicates in rules.ts decide detection, the matrix decides what is emitted
|
|
6
|
+
// where and how loud (plan §"分面契约").
|
|
7
|
+
//
|
|
8
|
+
// Native emission per the matrix:
|
|
9
|
+
// * tree facet → MEDIUM family only (home-path, cred-location,
|
|
10
|
+
// host-profile). The HIGH family's tree face belongs to
|
|
11
|
+
// the twin engines (T2); the v2 native double-emission
|
|
12
|
+
// was revoked, and a native tree rfc1918/ssh-target
|
|
13
|
+
// finding is UNREPRESENTABLE here (emitRulesFor returns
|
|
14
|
+
// none for those cells).
|
|
15
|
+
// * message facet → all five rules under the `exfil-*:message` id family;
|
|
16
|
+
// attribution `source` is the commit sha, which the
|
|
17
|
+
// pipeline passes through toRepoRelative unchanged
|
|
18
|
+
// (exclusions.ts:20-25), enabling per-commit allow rows.
|
|
19
|
+
// * tagNote facet → never scanned by the core (engine tag-message-secret
|
|
20
|
+
// leg owns it, tagScan.ts:26,101); there is deliberately
|
|
21
|
+
// no scanTagNoteText export.
|
|
22
|
+
//
|
|
23
|
+
// G23 masking invariant: the matched value leaves this module only as
|
|
24
|
+
// {valueDigest, snippet}. maskValue() mirrors redact.ts exactly (code-point
|
|
25
|
+
// based, ≤12 fully masked, else first4…last4) but is re-implemented locally
|
|
26
|
+
// because the import-audit pins src/exfil/** to node builtins + local files;
|
|
27
|
+
// the equivalence is pinned by a guard test against the real redact().
|
|
28
|
+
import { createHash } from "node:crypto";
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
emitRulesFor,
|
|
32
|
+
exfilFindingId,
|
|
33
|
+
EXFIL_SECRET_VALUE_RULES,
|
|
34
|
+
observedSeverity,
|
|
35
|
+
type ExfilChannel,
|
|
36
|
+
type ExfilFacet,
|
|
37
|
+
type ExfilRuleId,
|
|
38
|
+
type ExfilSeverity,
|
|
39
|
+
} from "./severity.ts";
|
|
40
|
+
import {
|
|
41
|
+
matchCredLocation,
|
|
42
|
+
matchHomePath,
|
|
43
|
+
matchHostProfile,
|
|
44
|
+
matchRfc1918,
|
|
45
|
+
matchSshTarget,
|
|
46
|
+
type RuleHit,
|
|
47
|
+
type RuleOptions,
|
|
48
|
+
} from "./rules.ts";
|
|
49
|
+
|
|
50
|
+
/** Engine label for native exfil findings (mirrors "ai-artifacts" naming style). */
|
|
51
|
+
export const EXFIL_ENGINE = "border-exfil";
|
|
52
|
+
|
|
53
|
+
const FULL_MASK = "\u25ae\u25ae\u25ae\u25ae";
|
|
54
|
+
const ELLIPSIS = "\u2026";
|
|
55
|
+
|
|
56
|
+
/** redact.ts-equivalent masking (G23): sha256 digest + code-point-safe snippet. */
|
|
57
|
+
export function maskValue(value: string): { valueDigest: string; snippet: string } {
|
|
58
|
+
const valueDigest = createHash("sha256").update(value, "utf8").digest("hex");
|
|
59
|
+
const points = [...value];
|
|
60
|
+
const snippet = points.length <= 12 ? FULL_MASK : `${points.slice(0, 4).join("")}${ELLIPSIS}${points.slice(-4).join("")}`;
|
|
61
|
+
return { valueDigest, snippet };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type ExfilScanInput = {
|
|
65
|
+
/** the text being audited (blob content or one commit message). */
|
|
66
|
+
readonly text: string;
|
|
67
|
+
/** attribution: file path (tree) or commit sha (message). */
|
|
68
|
+
readonly source: string;
|
|
69
|
+
/** operator's rules.hosts (config.ts:102) for the ssh-target predicate. */
|
|
70
|
+
readonly hosts?: readonly string[];
|
|
71
|
+
/**
|
|
72
|
+
* Ingest hook mirroring the redact.ts adapter contract ("every adapter
|
|
73
|
+
* registers every value it ingests"): the check pipeline passes
|
|
74
|
+
* (raw, digest) => sanitizer.register(digest, raw) so flagged literals are
|
|
75
|
+
* scrubbed from all rendered text; stays local so the import-audit holds.
|
|
76
|
+
*/
|
|
77
|
+
readonly onMatch?: (rawValue: string, valueDigest: string) => void;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type ExfilFinding = {
|
|
81
|
+
/** severity.ts-resolved id: bare rule on tree, `exfil-*:message` on messages. */
|
|
82
|
+
readonly id: string;
|
|
83
|
+
readonly rule: ExfilRuleId;
|
|
84
|
+
readonly severity: ExfilSeverity;
|
|
85
|
+
readonly facet: ExfilFacet;
|
|
86
|
+
readonly channel: ExfilChannel;
|
|
87
|
+
/** parity with Finding.engine: string (src/findings.ts); native legs stamp EXFIL_ENGINE. */
|
|
88
|
+
readonly engine: string;
|
|
89
|
+
readonly message: string;
|
|
90
|
+
readonly source: string;
|
|
91
|
+
readonly line: number;
|
|
92
|
+
readonly valueDigest: string;
|
|
93
|
+
readonly snippet: string;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const PREDICATES: Readonly<Record<ExfilRuleId, (text: string, o: RuleOptions) => RuleHit[]>> = {
|
|
97
|
+
"exfil-rfc1918": matchRfc1918,
|
|
98
|
+
"exfil-ssh-target": matchSshTarget,
|
|
99
|
+
"exfil-home-path": matchHomePath,
|
|
100
|
+
"exfil-cred-location": matchCredLocation,
|
|
101
|
+
"exfil-host-profile": matchHostProfile,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* One emitter for both facets: iterate exactly the rules the matrix ALLOWS for
|
|
106
|
+
* (facet × native), stamp severity from the same lookup (single source, no
|
|
107
|
+
* re-decision), and mask the evidence. Deterministic order: line, then id,
|
|
108
|
+
* then digest.
|
|
109
|
+
*/
|
|
110
|
+
function scanFacet(input: ExfilScanInput, facet: Extract<ExfilFacet, "tree" | "message">): ExfilFinding[] {
|
|
111
|
+
const channel: ExfilChannel = "native";
|
|
112
|
+
const options: RuleOptions = { facet, ...(input.hosts !== undefined ? { hosts: input.hosts } : {}) };
|
|
113
|
+
const findings: ExfilFinding[] = [];
|
|
114
|
+
for (const rule of emitRulesFor(facet, channel)) {
|
|
115
|
+
const severity = observedSeverity(rule, facet, channel);
|
|
116
|
+
if (severity === null) continue; // emitRulesFor excludes nulls; type narrowing kept honest.
|
|
117
|
+
for (const hit of PREDICATES[rule](input.text, options)) {
|
|
118
|
+
const { valueDigest, snippet } = maskValue(hit.matched);
|
|
119
|
+
if (EXFIL_SECRET_VALUE_RULES.includes(rule)) input.onMatch?.(hit.matched, valueDigest); // F4: location class never registers
|
|
120
|
+
findings.push({
|
|
121
|
+
id: exfilFindingId(rule, facet),
|
|
122
|
+
rule,
|
|
123
|
+
severity,
|
|
124
|
+
facet,
|
|
125
|
+
channel,
|
|
126
|
+
engine: EXFIL_ENGINE,
|
|
127
|
+
message: hit.message,
|
|
128
|
+
source: input.source,
|
|
129
|
+
line: hit.line,
|
|
130
|
+
valueDigest,
|
|
131
|
+
snippet,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return findings.sort((a, b) => a.line - b.line || a.id.localeCompare(b.id) || a.valueDigest.localeCompare(b.valueDigest));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Native tree/blob leg: MEDIUM-family rules only, per the matrix (HIGH family = twins). */
|
|
139
|
+
export function scanTreeText(input: ExfilScanInput): ExfilFinding[] {
|
|
140
|
+
return scanFacet(input, "tree");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Commit-message leg: all five rules under the `:message` id family, sha attribution. */
|
|
144
|
+
export function scanMessageText(input: ExfilScanInput): ExfilFinding[] {
|
|
145
|
+
return scanFacet(input, "message");
|
|
146
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// provenance: .omo/plans/border-exfil-lens.md T1 — the single home of the
|
|
2
|
+
// (rule × facet × channel → observed severity) matrix, plan §"分面契约" (R2).
|
|
3
|
+
//
|
|
4
|
+
// THE MATRIX IS THE PRODUCT DECISION and lives here and nowhere else; the
|
|
5
|
+
// later README rule table (T8) and the check-pipeline stages (T2/T3) render
|
|
6
|
+
// this table, they never re-decide it. Cell semantics:
|
|
7
|
+
//
|
|
8
|
+
// * tree facet, twin channels — the HIGH family (exfil-rfc1918,
|
|
9
|
+
// exfil-ssh-target) rides the vendored gitleaks TOML + secretlint border
|
|
10
|
+
// patterns (T2 ships them with the ids VERBATIM equal to these rule ids).
|
|
11
|
+
// Observed severity is CRITICAL, not the table HIGH, because that is the
|
|
12
|
+
// channels' mechanical truth: gitleaks.ts:81 hardcodes severity
|
|
13
|
+
// "CRITICAL" for every finding it ingests, and secretlint.ts severityOf
|
|
14
|
+
// (299-309) maps error→CRITICAL with NO HIGH rung ("不硬掰,如实表").
|
|
15
|
+
// * tree facet, native channel — the MEDIUM family (exfil-home-path,
|
|
16
|
+
// exfil-cred-location, exfil-host-profile) only. The v2 native tree/blob
|
|
17
|
+
// double-emission of the HIGH family was REVOKED in plan REV v3
|
|
18
|
+
// ("撤销 v2 的 native 树/blob 双发 stage"): source-level elimination of
|
|
19
|
+
// triple-firing. A null cell here is a FORBIDDEN emission, and the guard
|
|
20
|
+
// tests pin it.
|
|
21
|
+
// * message facet — native EXCLUSIVE (`exfil-*:message` id family, T3's
|
|
22
|
+
// messageScan leg): all five rules at their table severity. The twins do
|
|
23
|
+
// not scan commit messages; a (message, twin) cell is null.
|
|
24
|
+
// * tagNote facet — owned by the EXISTING engine leg: the tagScan stdin leg
|
|
25
|
+
// (tagScan.ts:26,101) rides the vendored TOML and re-labels every hit
|
|
26
|
+
// `tag-message-secret` CRITICAL (path = the tag refname). The core
|
|
27
|
+
// therefore NEVER scans tag notes and no exfil-* finding id may be
|
|
28
|
+
// observed on this facet — every tagNote cell is null, eliminating
|
|
29
|
+
// double-firing by construction. The `:message` family covers commit
|
|
30
|
+
// messages only.
|
|
31
|
+
//
|
|
32
|
+
// Isolation note (plan §分面契约): allow-list entries are matched EXACTLY on
|
|
33
|
+
// the rule id (allow.ts:38 → matchGlob with no wildcards in a plain id), so a
|
|
34
|
+
// blob-face exemption `{rule: "exfil-rfc1918"}` never pierces the
|
|
35
|
+
// `exfil-rfc1918:message` id and vice versa — the ids are kept structurally
|
|
36
|
+
// distinct HERE (exfilFindingId) so the two families can never collide.
|
|
37
|
+
//
|
|
38
|
+
// Import-audit (plan §分面契约 last bullet): this directory imports only node
|
|
39
|
+
// builtins and local exfil files — hence the severity union is re-declared
|
|
40
|
+
// here instead of importing src/findings.ts; T2's pipeline maps these cells
|
|
41
|
+
// onto Finding verbatim, and the guard test pins ordering compatibility.
|
|
42
|
+
|
|
43
|
+
/** Mirrors SEVERITIES in src/findings.ts (re-declared: exfil core stays import-free). */
|
|
44
|
+
export const EXFIL_SEVERITIES = ["INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL"] as const;
|
|
45
|
+
export type ExfilSeverity = (typeof EXFIL_SEVERITIES)[number];
|
|
46
|
+
|
|
47
|
+
/** The five exfil-lens rule ids, verbatim; twins must ship these ids byte-equal (T2 guard). */
|
|
48
|
+
export const EXFIL_RULE_IDS = [
|
|
49
|
+
"exfil-rfc1918",
|
|
50
|
+
"exfil-ssh-target",
|
|
51
|
+
"exfil-home-path",
|
|
52
|
+
"exfil-cred-location",
|
|
53
|
+
"exfil-host-profile",
|
|
54
|
+
] as const;
|
|
55
|
+
export type ExfilRuleId = (typeof EXFIL_RULE_IDS)[number];
|
|
56
|
+
|
|
57
|
+
/** Message-facet attribution id family, e.g. "exfil-rfc1918:message" (plan §分面契约 row 2). */
|
|
58
|
+
export const EXFIL_MESSAGE_ID_SUFFIX = ":message";
|
|
59
|
+
export type ExfilMessageRuleId = `${ExfilRuleId}:message`;
|
|
60
|
+
|
|
61
|
+
export const EXFIL_FACETS = ["tree", "message", "tagNote"] as const;
|
|
62
|
+
export type ExfilFacet = (typeof EXFIL_FACETS)[number];
|
|
63
|
+
|
|
64
|
+
export const EXFIL_CHANNELS = ["native", "gitleaks-twin", "secretlint-twin"] as const;
|
|
65
|
+
export type ExfilChannel = (typeof EXFIL_CHANNELS)[number];
|
|
66
|
+
|
|
67
|
+
/** Intent severities from the plan's 规则语义表 (what the rule means, before channel mechanics). */
|
|
68
|
+
export const EXFIL_TABLE_SEVERITY: Readonly<Record<ExfilRuleId, ExfilSeverity>> = {
|
|
69
|
+
"exfil-rfc1918": "HIGH",
|
|
70
|
+
"exfil-ssh-target": "HIGH",
|
|
71
|
+
"exfil-home-path": "MEDIUM",
|
|
72
|
+
"exfil-cred-location": "MEDIUM",
|
|
73
|
+
"exfil-host-profile": "MEDIUM",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** HIGH family: the two identity-carrying location rules; tree face belongs to the twins. */
|
|
77
|
+
export const EXFIL_HIGH_FAMILY: readonly ExfilRuleId[] = ["exfil-rfc1918", "exfil-ssh-target"];
|
|
78
|
+
/**
|
|
79
|
+
* Identifier class (F4): the matched text is a secret-shaped VALUE (an
|
|
80
|
+
* internal IP, a user@internal-host) — the check legs register these with the
|
|
81
|
+
* run's TextSanitizer so the literal cannot resurface in rendered output.
|
|
82
|
+
* The MEDIUM location class must NEVER register: there the matched text is a
|
|
83
|
+
* path whose literal printing IS the finding, and registering it turns every
|
|
84
|
+
* later render of that path into a [REDACTED storm (dogfood, round 2).
|
|
85
|
+
*/
|
|
86
|
+
export const EXFIL_SECRET_VALUE_RULES: readonly ExfilRuleId[] = EXFIL_HIGH_FAMILY;
|
|
87
|
+
/** MEDIUM family: privacy-shape rules; native carries them on both scanned facets. */
|
|
88
|
+
export const EXFIL_MEDIUM_FAMILY: readonly ExfilRuleId[] = ["exfil-home-path", "exfil-cred-location", "exfil-host-profile"];
|
|
89
|
+
|
|
90
|
+
/** null = FORBIDDEN cell: that channel must never emit that rule on that facet. */
|
|
91
|
+
export type ObservedSeverity = ExfilSeverity | null;
|
|
92
|
+
|
|
93
|
+
const TWIN_HIGH: Readonly<Record<ExfilRuleId, ObservedSeverity>> = {
|
|
94
|
+
"exfil-rfc1918": "CRITICAL",
|
|
95
|
+
"exfil-ssh-target": "CRITICAL",
|
|
96
|
+
"exfil-home-path": null,
|
|
97
|
+
"exfil-cred-location": null,
|
|
98
|
+
"exfil-host-profile": null,
|
|
99
|
+
};
|
|
100
|
+
const NULL_ROW: Readonly<Record<ExfilRuleId, ObservedSeverity>> = {
|
|
101
|
+
"exfil-rfc1918": null,
|
|
102
|
+
"exfil-ssh-target": null,
|
|
103
|
+
"exfil-home-path": null,
|
|
104
|
+
"exfil-cred-location": null,
|
|
105
|
+
"exfil-host-profile": null,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** The full (facet × channel × rule) truth table — the plan's 分面契约, machine-readable. */
|
|
109
|
+
export const EXFIL_MATRIX: Readonly<Record<ExfilFacet, Readonly<Record<ExfilChannel, Readonly<Record<ExfilRuleId, ObservedSeverity>>>>>> = {
|
|
110
|
+
tree: {
|
|
111
|
+
// native tree leg = MEDIUM family补发 only (v2 double-emission revoked).
|
|
112
|
+
native: { ...NULL_ROW, "exfil-home-path": "MEDIUM", "exfil-cred-location": "MEDIUM", "exfil-host-profile": "MEDIUM" },
|
|
113
|
+
// gitleaks.ts:81 hardcodes CRITICAL for every ingested finding.
|
|
114
|
+
"gitleaks-twin": TWIN_HIGH,
|
|
115
|
+
// secretlint.ts:299-309 has no HIGH rung — error maps to CRITICAL as-is.
|
|
116
|
+
"secretlint-twin": TWIN_HIGH,
|
|
117
|
+
},
|
|
118
|
+
message: {
|
|
119
|
+
// native-exclusive `:message` family at table severity.
|
|
120
|
+
native: { ...EXFIL_TABLE_SEVERITY },
|
|
121
|
+
"gitleaks-twin": NULL_ROW,
|
|
122
|
+
"secretlint-twin": NULL_ROW,
|
|
123
|
+
},
|
|
124
|
+
tagNote: {
|
|
125
|
+
// engine既有腿 owns this facet as `tag-message-secret` (a non-exfil id);
|
|
126
|
+
// NO exfil-* emission is legal on tag notes through ANY channel.
|
|
127
|
+
native: NULL_ROW,
|
|
128
|
+
"gitleaks-twin": NULL_ROW,
|
|
129
|
+
"secretlint-twin": NULL_ROW,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** Observed severity for one (rule × facet × channel) cell; null = that emission is forbidden. */
|
|
134
|
+
export function observedSeverity(rule: ExfilRuleId, facet: ExfilFacet, channel: ExfilChannel): ObservedSeverity {
|
|
135
|
+
return EXFIL_MATRIX[facet][channel][rule];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Every rule a (facet × channel) combination is ALLOWED to emit, in EXFIL_RULE_IDS order. */
|
|
139
|
+
export function emitRulesFor(facet: ExfilFacet, channel: ExfilChannel): readonly ExfilRuleId[] {
|
|
140
|
+
return EXFIL_RULE_IDS.filter((rule) => observedSeverity(rule, facet, channel) !== null);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The finding id for a (rule × facet) pair: the `:message` family ONLY on the
|
|
145
|
+
* message facet; tree/tagNote carry the bare rule id (the id must stay
|
|
146
|
+
* exactly "exfil-*" for the twin-id-equality and blob-face allow-list match).
|
|
147
|
+
*/
|
|
148
|
+
export function exfilFindingId(rule: ExfilRuleId, facet: ExfilFacet): string {
|
|
149
|
+
switch (facet) {
|
|
150
|
+
case "message":
|
|
151
|
+
return `${rule}${EXFIL_MESSAGE_ID_SUFFIX}`;
|
|
152
|
+
case "tree":
|
|
153
|
+
case "tagNote":
|
|
154
|
+
return rule;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -3207,3 +3207,53 @@ description = "Detected a Zendesk Secret Key, risking unauthorized access to cus
|
|
|
3207
3207
|
regex = '''(?i)[\w.-]{0,50}?(?:zendesk)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)'''
|
|
3208
3208
|
keywords = ["zendesk"]
|
|
3209
3209
|
|
|
3210
|
+
# ---------------------------------------------------------------------------
|
|
3211
|
+
# border vendored additions (NOT upstream-generated; plan
|
|
3212
|
+
# .omo/plans/border-exfil-lens.md T2 — exfil lens tree-facet HIGH family, the
|
|
3213
|
+
# "gitleaks-twin" channel). Ids must stay VERBATIM equal to src/exfil rule ids
|
|
3214
|
+
# (EXFIL_HIGH_FAMILY); the twin guard parses this file. Go RE2 has no
|
|
3215
|
+
# lookaround, so boundaries use NON-CAPTURING character classes and the token
|
|
3216
|
+
# itself rides (?P<secret>…): that is what gitleaks 8.30.1 digests as Secret
|
|
3217
|
+
# (border's adapter hashes Secret over Match) and what per-rule allowlists are
|
|
3218
|
+
# matched against by default. The adjacent-IP edge (second IP on one line
|
|
3219
|
+
# shadowed by the consumed separator) is covered by the secretlint twin's JS
|
|
3220
|
+
# lookarounds. No literal dotted quads appear here — octet classes only
|
|
3221
|
+
# (self-stealth).
|
|
3222
|
+
# F2 VERDICT PARITY: the per-rule allowlists cancel findings whose secret is
|
|
3223
|
+
# an exempt-band address — loopback, each RFC5737 /24, and the 10.200/16
|
|
3224
|
+
# synthetic-fixture band (mirrors src/exfil DEFAULT_EXEMPT_BANDS; corpus-pinned
|
|
3225
|
+
# by the VERDICT-PARITY test in test/exfil.pipeline.test.ts). Anchored ^ on the
|
|
3226
|
+
# secret; ssh secrets are "user@host", so their patterns anchor at the @.
|
|
3227
|
+
# ---------------------------------------------------------------------------
|
|
3228
|
+
|
|
3229
|
+
[[rules]]
|
|
3230
|
+
id = "exfil-rfc1918"
|
|
3231
|
+
description = "RFC1918 private-network address literal on a surface about to become public (border exfil lens)."
|
|
3232
|
+
regex = '''(?:^|[^0-9A-Za-z.])(?P<secret>10\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){2}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|172\.(?:1[6-9]|2[0-9]|3[01])\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.)(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|192\.168\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.)(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]))(?:[^0-9A-Za-z.]|$)'''
|
|
3233
|
+
keywords = ["10.", "172.", "192."]
|
|
3234
|
+
severity = "critical"
|
|
3235
|
+
[[rules.allowlists]]
|
|
3236
|
+
description = "border exfil F2 parity: exempt bands (127/8, RFC5737 /24s, 10.200/16 synthetic-fixture band)"
|
|
3237
|
+
regexes = [
|
|
3238
|
+
'''^127\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){2}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$''',
|
|
3239
|
+
'''^192\.0\.2\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$''',
|
|
3240
|
+
'''^198\.51\.100\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$''',
|
|
3241
|
+
'''^203\.0\.113\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$''',
|
|
3242
|
+
'''^10\.200\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.)(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$''',
|
|
3243
|
+
]
|
|
3244
|
+
|
|
3245
|
+
[[rules]]
|
|
3246
|
+
id = "exfil-ssh-target"
|
|
3247
|
+
description = "SSH-style user@host naming an internal-only target: RFC1918 literal or private suffix (.internal/.lan/.local/.corp/.intra; RFC2606 shapes cannot match a suffix at word end)."
|
|
3248
|
+
regex = '''(?:^|[^0-9A-Za-z._+%-])(?P<secret>[0-9A-Za-z._+%-]+@(?:10\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){2}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|172\.(?:1[6-9]|2[0-9]|3[01])\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.)(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|192\.168\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.)(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|[0-9A-Za-z](?:[0-9A-Za-z-]*[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:[0-9A-Za-z-]*[0-9A-Za-z])?)*\.(?:internal|lan|local|corp|intra)))(?:[^0-9A-Za-z._-]|$)'''
|
|
3249
|
+
keywords = [".internal", ".lan", ".local", ".corp", ".intra", "10.", "172.", "192."]
|
|
3250
|
+
severity = "critical"
|
|
3251
|
+
[[rules.allowlists]]
|
|
3252
|
+
description = "border exfil F2 parity: exempt-band IP hosts never count as internal ssh targets on the tree facet (10.200/16 fixture band et al.)"
|
|
3253
|
+
regexes = [
|
|
3254
|
+
'''^[0-9A-Za-z._+%-]+@127\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){2}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$''',
|
|
3255
|
+
'''^[0-9A-Za-z._+%-]+@192\.0\.2\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$''',
|
|
3256
|
+
'''^[0-9A-Za-z._+%-]+@198\.51\.100\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$''',
|
|
3257
|
+
'''^[0-9A-Za-z._+%-]+@203\.0\.113\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$''',
|
|
3258
|
+
'''^[0-9A-Za-z._+%-]+@10\.200\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.)(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$''',
|
|
3259
|
+
]
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "border-customs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "border-customs",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.7.1",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@opencode-ai/plugin": "^1.17.4",
|