@codyswann/lisa 2.146.1 → 2.147.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/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/schema/lisa-wiki-config.schema.json +46 -2
- package/plugins/lisa-wiki/scripts/lint-wiki.mjs +137 -0
- package/plugins/lisa-wiki/scripts/validate-config.mjs +89 -0
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/schema/lisa-wiki-config.schema.json +46 -2
- package/plugins/lisa-wiki-agy/scripts/lint-wiki.mjs +137 -0
- package/plugins/lisa-wiki-agy/scripts/validate-config.mjs +89 -0
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/schema/lisa-wiki-config.schema.json +46 -2
- package/plugins/lisa-wiki-copilot/scripts/lint-wiki.mjs +137 -0
- package/plugins/lisa-wiki-copilot/scripts/validate-config.mjs +89 -0
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/schema/lisa-wiki-config.schema.json +46 -2
- package/plugins/lisa-wiki-cursor/scripts/lint-wiki.mjs +137 -0
- package/plugins/lisa-wiki-cursor/scripts/validate-config.mjs +89 -0
- package/plugins/src/wiki/schema/lisa-wiki-config.schema.json +46 -2
- package/plugins/src/wiki/scripts/lint-wiki.mjs +137 -0
- package/plugins/src/wiki/scripts/validate-config.mjs +89 -0
|
@@ -22,6 +22,17 @@ const RETENTION = [
|
|
|
22
22
|
"external-pointer-only",
|
|
23
23
|
];
|
|
24
24
|
const SENSITIVITY = ["public", "internal", "confidential", "restricted"];
|
|
25
|
+
const REDACTION_ENTITIES = [
|
|
26
|
+
"api_key",
|
|
27
|
+
"bank_account",
|
|
28
|
+
"credit_card",
|
|
29
|
+
"oauth_token",
|
|
30
|
+
"password",
|
|
31
|
+
"private_key",
|
|
32
|
+
"routing_number",
|
|
33
|
+
"ssn",
|
|
34
|
+
];
|
|
35
|
+
const REDACTION_SCANNERS = ["builtin", "gitleaks", "presidio"];
|
|
25
36
|
const SOURCE_LAYOUT = ["by-system", "by-category"];
|
|
26
37
|
const README_MODE = ["rich", "stub", "preserve"];
|
|
27
38
|
|
|
@@ -49,6 +60,14 @@ function checkType(value, type, label) {
|
|
|
49
60
|
);
|
|
50
61
|
}
|
|
51
62
|
}
|
|
63
|
+
function checkKnownKeys(object, allowed, label) {
|
|
64
|
+
if (!isObject(object)) return;
|
|
65
|
+
for (const key of Object.keys(object)) {
|
|
66
|
+
if (!allowed.includes(key)) {
|
|
67
|
+
err(`${label}.${key}: unknown field`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
52
71
|
|
|
53
72
|
if (!fs.existsSync(configPath)) {
|
|
54
73
|
console.error(`✗ config not found: ${configPath}`);
|
|
@@ -123,8 +142,78 @@ if (config.readme !== undefined) {
|
|
|
123
142
|
if (config.sensitivity !== undefined) {
|
|
124
143
|
if (!isObject(config.sensitivity)) err("sensitivity: must be an object");
|
|
125
144
|
else {
|
|
145
|
+
checkKnownKeys(
|
|
146
|
+
config.sensitivity,
|
|
147
|
+
["enabled", "default", "redaction"],
|
|
148
|
+
"sensitivity"
|
|
149
|
+
);
|
|
126
150
|
checkType(config.sensitivity.enabled, "boolean", "sensitivity.enabled");
|
|
127
151
|
checkEnum(config.sensitivity.default, SENSITIVITY, "sensitivity.default");
|
|
152
|
+
if (config.sensitivity.redaction !== undefined) {
|
|
153
|
+
if (!isObject(config.sensitivity.redaction)) {
|
|
154
|
+
err("sensitivity.redaction: must be an object");
|
|
155
|
+
} else {
|
|
156
|
+
const redaction = config.sensitivity.redaction;
|
|
157
|
+
checkKnownKeys(
|
|
158
|
+
redaction,
|
|
159
|
+
[
|
|
160
|
+
"enabled",
|
|
161
|
+
"scanners",
|
|
162
|
+
"failClosed",
|
|
163
|
+
"requireReview",
|
|
164
|
+
"allowedEntities",
|
|
165
|
+
"blockedEntities",
|
|
166
|
+
],
|
|
167
|
+
"sensitivity.redaction"
|
|
168
|
+
);
|
|
169
|
+
checkType(
|
|
170
|
+
redaction.enabled,
|
|
171
|
+
"boolean",
|
|
172
|
+
"sensitivity.redaction.enabled"
|
|
173
|
+
);
|
|
174
|
+
checkType(
|
|
175
|
+
redaction.failClosed,
|
|
176
|
+
"boolean",
|
|
177
|
+
"sensitivity.redaction.failClosed"
|
|
178
|
+
);
|
|
179
|
+
checkType(
|
|
180
|
+
redaction.requireReview,
|
|
181
|
+
"boolean",
|
|
182
|
+
"sensitivity.redaction.requireReview"
|
|
183
|
+
);
|
|
184
|
+
if (
|
|
185
|
+
redaction.scanners !== undefined &&
|
|
186
|
+
!(isStringArray(redaction.scanners) && redaction.scanners.length > 0)
|
|
187
|
+
) {
|
|
188
|
+
err(
|
|
189
|
+
"sensitivity.redaction.scanners: must be a non-empty array of strings"
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const scanners = isStringArray(redaction.scanners)
|
|
193
|
+
? redaction.scanners
|
|
194
|
+
: [];
|
|
195
|
+
for (const scanner of scanners) {
|
|
196
|
+
checkEnum(
|
|
197
|
+
scanner,
|
|
198
|
+
REDACTION_SCANNERS,
|
|
199
|
+
"sensitivity.redaction.scanners[]"
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
for (const key of ["allowedEntities", "blockedEntities"]) {
|
|
203
|
+
if (redaction[key] !== undefined && !isStringArray(redaction[key])) {
|
|
204
|
+
err(`sensitivity.redaction.${key}: must be an array of strings`);
|
|
205
|
+
}
|
|
206
|
+
const entities = isStringArray(redaction[key]) ? redaction[key] : [];
|
|
207
|
+
for (const entity of entities) {
|
|
208
|
+
checkEnum(
|
|
209
|
+
entity,
|
|
210
|
+
REDACTION_ENTITIES,
|
|
211
|
+
`sensitivity.redaction.${key}[]`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
128
217
|
}
|
|
129
218
|
}
|
|
130
219
|
if (config.documentation !== undefined) {
|
|
@@ -35,10 +35,54 @@
|
|
|
35
35
|
},
|
|
36
36
|
"sensitivity": {
|
|
37
37
|
"type": "object",
|
|
38
|
-
"additionalProperties":
|
|
38
|
+
"additionalProperties": false,
|
|
39
39
|
"properties": {
|
|
40
40
|
"enabled": { "type": "boolean" },
|
|
41
|
-
"default": { "enum": ["public", "internal", "confidential", "restricted"] }
|
|
41
|
+
"default": { "enum": ["public", "internal", "confidential", "restricted"] },
|
|
42
|
+
"redaction": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"additionalProperties": false,
|
|
45
|
+
"properties": {
|
|
46
|
+
"enabled": { "type": "boolean" },
|
|
47
|
+
"scanners": {
|
|
48
|
+
"type": "array",
|
|
49
|
+
"items": { "enum": ["builtin", "gitleaks", "presidio"] },
|
|
50
|
+
"minItems": 1
|
|
51
|
+
},
|
|
52
|
+
"failClosed": { "type": "boolean" },
|
|
53
|
+
"requireReview": { "type": "boolean" },
|
|
54
|
+
"allowedEntities": {
|
|
55
|
+
"type": "array",
|
|
56
|
+
"items": {
|
|
57
|
+
"enum": [
|
|
58
|
+
"api_key",
|
|
59
|
+
"bank_account",
|
|
60
|
+
"credit_card",
|
|
61
|
+
"oauth_token",
|
|
62
|
+
"password",
|
|
63
|
+
"private_key",
|
|
64
|
+
"routing_number",
|
|
65
|
+
"ssn"
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"blockedEntities": {
|
|
70
|
+
"type": "array",
|
|
71
|
+
"items": {
|
|
72
|
+
"enum": [
|
|
73
|
+
"api_key",
|
|
74
|
+
"bank_account",
|
|
75
|
+
"credit_card",
|
|
76
|
+
"oauth_token",
|
|
77
|
+
"password",
|
|
78
|
+
"private_key",
|
|
79
|
+
"routing_number",
|
|
80
|
+
"ssn"
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
42
86
|
}
|
|
43
87
|
},
|
|
44
88
|
"sourceRetention": { "enum": ["raw-ok", "sanitized-note-only", "metadata-only", "external-pointer-only"] },
|
|
@@ -15,6 +15,7 @@ import { fileURLToPath } from "node:url";
|
|
|
15
15
|
import {
|
|
16
16
|
loadConfig,
|
|
17
17
|
loadStructure,
|
|
18
|
+
readJsonSafe,
|
|
18
19
|
pluginRootFrom,
|
|
19
20
|
walkFiles,
|
|
20
21
|
parseFrontmatter,
|
|
@@ -60,6 +61,14 @@ const rel = p => path.relative(process.cwd(), p);
|
|
|
60
61
|
const wrel = p => path.relative(wikiRoot, p);
|
|
61
62
|
const categories = config?.categories ?? structure.categoryDirs?.default ?? [];
|
|
62
63
|
const frontmatterRequired = config?.frontmatter !== false;
|
|
64
|
+
const configPath = path.resolve(
|
|
65
|
+
opt("--config") ?? "wiki/lisa-wiki.config.json"
|
|
66
|
+
);
|
|
67
|
+
const localConfigPath = path.join(
|
|
68
|
+
path.dirname(configPath),
|
|
69
|
+
"lisa-wiki.config.local.json"
|
|
70
|
+
);
|
|
71
|
+
const localConfig = readJsonSafe(localConfigPath);
|
|
63
72
|
|
|
64
73
|
const allMd = walkFiles(wikiRoot, { ext: ".md" });
|
|
65
74
|
const allFiles = walkFiles(wikiRoot);
|
|
@@ -71,6 +80,134 @@ const isUnder = (p, dir) => {
|
|
|
71
80
|
const isSynthesisPage = p => categories.some(c => isUnder(p, c));
|
|
72
81
|
const isSourceNote = p => isUnder(p, "sources");
|
|
73
82
|
|
|
83
|
+
// --- 0. redaction policy readiness ----------------------------------------
|
|
84
|
+
const availableRedactionScanners = new Set(["builtin"]);
|
|
85
|
+
const committedRedaction = config?.sensitivity?.redaction;
|
|
86
|
+
const localRedaction = localConfig?.sensitivity?.redaction;
|
|
87
|
+
const addUnsafeLocalOverride = message =>
|
|
88
|
+
report.add(
|
|
89
|
+
"redaction-policy",
|
|
90
|
+
"unsafe-local-override",
|
|
91
|
+
"FAIL",
|
|
92
|
+
message,
|
|
93
|
+
path.relative(wikiRoot, localConfigPath)
|
|
94
|
+
);
|
|
95
|
+
const isArraySubset = (candidate, baseline) =>
|
|
96
|
+
candidate.every(value => baseline.includes(value));
|
|
97
|
+
if (committedRedaction?.enabled === true) {
|
|
98
|
+
const scanners = Array.isArray(committedRedaction.scanners)
|
|
99
|
+
? committedRedaction.scanners
|
|
100
|
+
: [];
|
|
101
|
+
const missingScanners = scanners.filter(
|
|
102
|
+
scanner => !availableRedactionScanners.has(scanner)
|
|
103
|
+
);
|
|
104
|
+
if (scanners.length === 0) {
|
|
105
|
+
report.add(
|
|
106
|
+
"redaction-policy",
|
|
107
|
+
"scanner-missing",
|
|
108
|
+
committedRedaction.failClosed === false ? "WARN" : "FAIL",
|
|
109
|
+
"redaction is enabled but no scanner is selected"
|
|
110
|
+
);
|
|
111
|
+
} else if (missingScanners.length > 0) {
|
|
112
|
+
report.add(
|
|
113
|
+
"redaction-policy",
|
|
114
|
+
"scanner-unavailable",
|
|
115
|
+
committedRedaction.failClosed === false ? "WARN" : "FAIL",
|
|
116
|
+
`redaction scanner unavailable: ${missingScanners.join(", ")}`
|
|
117
|
+
);
|
|
118
|
+
} else {
|
|
119
|
+
report.add(
|
|
120
|
+
"redaction-policy",
|
|
121
|
+
"scanner-available",
|
|
122
|
+
"PASS",
|
|
123
|
+
`redaction scanner available: ${scanners.join(", ")}`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (committedRedaction.failClosed !== true) {
|
|
127
|
+
report.add(
|
|
128
|
+
"redaction-policy",
|
|
129
|
+
"fail-closed",
|
|
130
|
+
"WARN",
|
|
131
|
+
"redaction is enabled without failClosed=true"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (committedRedaction.requireReview !== true) {
|
|
135
|
+
report.add(
|
|
136
|
+
"redaction-policy",
|
|
137
|
+
"review-required",
|
|
138
|
+
"WARN",
|
|
139
|
+
"redaction is enabled without requireReview=true"
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (localRedaction?.enabled === false) {
|
|
143
|
+
addUnsafeLocalOverride("local config disables committed redaction policy");
|
|
144
|
+
}
|
|
145
|
+
if (
|
|
146
|
+
committedRedaction.failClosed === true &&
|
|
147
|
+
localRedaction?.failClosed === false
|
|
148
|
+
) {
|
|
149
|
+
addUnsafeLocalOverride(
|
|
150
|
+
"local config disables committed redaction fail-closed policy"
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (
|
|
154
|
+
committedRedaction.requireReview === true &&
|
|
155
|
+
localRedaction?.requireReview === false
|
|
156
|
+
) {
|
|
157
|
+
addUnsafeLocalOverride(
|
|
158
|
+
"local config disables committed redaction review requirement"
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (localRedaction && Array.isArray(localRedaction.scanners)) {
|
|
162
|
+
const localScanners = localRedaction.scanners;
|
|
163
|
+
const unavailableLocalScanners = localScanners.filter(
|
|
164
|
+
scanner => !availableRedactionScanners.has(scanner)
|
|
165
|
+
);
|
|
166
|
+
const removedScanners = scanners.filter(
|
|
167
|
+
scanner => !localScanners.includes(scanner)
|
|
168
|
+
);
|
|
169
|
+
if (scanners.length > 0 && localScanners.length === 0) {
|
|
170
|
+
addUnsafeLocalOverride(
|
|
171
|
+
"local config removes committed redaction scanners"
|
|
172
|
+
);
|
|
173
|
+
} else if (unavailableLocalScanners.length > 0) {
|
|
174
|
+
addUnsafeLocalOverride(
|
|
175
|
+
`local config selects unavailable redaction scanner: ${unavailableLocalScanners.join(", ")}`
|
|
176
|
+
);
|
|
177
|
+
} else if (removedScanners.length > 0) {
|
|
178
|
+
addUnsafeLocalOverride(
|
|
179
|
+
`local config removes committed redaction scanner: ${removedScanners.join(", ")}`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (
|
|
184
|
+
localRedaction &&
|
|
185
|
+
Array.isArray(committedRedaction.allowedEntities) &&
|
|
186
|
+
Array.isArray(localRedaction.allowedEntities) &&
|
|
187
|
+
!isArraySubset(
|
|
188
|
+
localRedaction.allowedEntities,
|
|
189
|
+
committedRedaction.allowedEntities
|
|
190
|
+
)
|
|
191
|
+
) {
|
|
192
|
+
addUnsafeLocalOverride(
|
|
193
|
+
"local config expands committed redaction allowed entities"
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (
|
|
197
|
+
localRedaction &&
|
|
198
|
+
Array.isArray(committedRedaction.blockedEntities) &&
|
|
199
|
+
Array.isArray(localRedaction.blockedEntities) &&
|
|
200
|
+
!isArraySubset(
|
|
201
|
+
committedRedaction.blockedEntities,
|
|
202
|
+
localRedaction.blockedEntities
|
|
203
|
+
)
|
|
204
|
+
) {
|
|
205
|
+
addUnsafeLocalOverride(
|
|
206
|
+
"local config removes committed redaction blocked entities"
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
74
211
|
// --- A. structure conformance ---------------------------------------------
|
|
75
212
|
for (const f of structure.requiredFiles ?? []) {
|
|
76
213
|
report.add(
|
|
@@ -22,6 +22,17 @@ const RETENTION = [
|
|
|
22
22
|
"external-pointer-only",
|
|
23
23
|
];
|
|
24
24
|
const SENSITIVITY = ["public", "internal", "confidential", "restricted"];
|
|
25
|
+
const REDACTION_ENTITIES = [
|
|
26
|
+
"api_key",
|
|
27
|
+
"bank_account",
|
|
28
|
+
"credit_card",
|
|
29
|
+
"oauth_token",
|
|
30
|
+
"password",
|
|
31
|
+
"private_key",
|
|
32
|
+
"routing_number",
|
|
33
|
+
"ssn",
|
|
34
|
+
];
|
|
35
|
+
const REDACTION_SCANNERS = ["builtin", "gitleaks", "presidio"];
|
|
25
36
|
const SOURCE_LAYOUT = ["by-system", "by-category"];
|
|
26
37
|
const README_MODE = ["rich", "stub", "preserve"];
|
|
27
38
|
|
|
@@ -49,6 +60,14 @@ function checkType(value, type, label) {
|
|
|
49
60
|
);
|
|
50
61
|
}
|
|
51
62
|
}
|
|
63
|
+
function checkKnownKeys(object, allowed, label) {
|
|
64
|
+
if (!isObject(object)) return;
|
|
65
|
+
for (const key of Object.keys(object)) {
|
|
66
|
+
if (!allowed.includes(key)) {
|
|
67
|
+
err(`${label}.${key}: unknown field`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
52
71
|
|
|
53
72
|
if (!fs.existsSync(configPath)) {
|
|
54
73
|
console.error(`✗ config not found: ${configPath}`);
|
|
@@ -123,8 +142,78 @@ if (config.readme !== undefined) {
|
|
|
123
142
|
if (config.sensitivity !== undefined) {
|
|
124
143
|
if (!isObject(config.sensitivity)) err("sensitivity: must be an object");
|
|
125
144
|
else {
|
|
145
|
+
checkKnownKeys(
|
|
146
|
+
config.sensitivity,
|
|
147
|
+
["enabled", "default", "redaction"],
|
|
148
|
+
"sensitivity"
|
|
149
|
+
);
|
|
126
150
|
checkType(config.sensitivity.enabled, "boolean", "sensitivity.enabled");
|
|
127
151
|
checkEnum(config.sensitivity.default, SENSITIVITY, "sensitivity.default");
|
|
152
|
+
if (config.sensitivity.redaction !== undefined) {
|
|
153
|
+
if (!isObject(config.sensitivity.redaction)) {
|
|
154
|
+
err("sensitivity.redaction: must be an object");
|
|
155
|
+
} else {
|
|
156
|
+
const redaction = config.sensitivity.redaction;
|
|
157
|
+
checkKnownKeys(
|
|
158
|
+
redaction,
|
|
159
|
+
[
|
|
160
|
+
"enabled",
|
|
161
|
+
"scanners",
|
|
162
|
+
"failClosed",
|
|
163
|
+
"requireReview",
|
|
164
|
+
"allowedEntities",
|
|
165
|
+
"blockedEntities",
|
|
166
|
+
],
|
|
167
|
+
"sensitivity.redaction"
|
|
168
|
+
);
|
|
169
|
+
checkType(
|
|
170
|
+
redaction.enabled,
|
|
171
|
+
"boolean",
|
|
172
|
+
"sensitivity.redaction.enabled"
|
|
173
|
+
);
|
|
174
|
+
checkType(
|
|
175
|
+
redaction.failClosed,
|
|
176
|
+
"boolean",
|
|
177
|
+
"sensitivity.redaction.failClosed"
|
|
178
|
+
);
|
|
179
|
+
checkType(
|
|
180
|
+
redaction.requireReview,
|
|
181
|
+
"boolean",
|
|
182
|
+
"sensitivity.redaction.requireReview"
|
|
183
|
+
);
|
|
184
|
+
if (
|
|
185
|
+
redaction.scanners !== undefined &&
|
|
186
|
+
!(isStringArray(redaction.scanners) && redaction.scanners.length > 0)
|
|
187
|
+
) {
|
|
188
|
+
err(
|
|
189
|
+
"sensitivity.redaction.scanners: must be a non-empty array of strings"
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const scanners = isStringArray(redaction.scanners)
|
|
193
|
+
? redaction.scanners
|
|
194
|
+
: [];
|
|
195
|
+
for (const scanner of scanners) {
|
|
196
|
+
checkEnum(
|
|
197
|
+
scanner,
|
|
198
|
+
REDACTION_SCANNERS,
|
|
199
|
+
"sensitivity.redaction.scanners[]"
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
for (const key of ["allowedEntities", "blockedEntities"]) {
|
|
203
|
+
if (redaction[key] !== undefined && !isStringArray(redaction[key])) {
|
|
204
|
+
err(`sensitivity.redaction.${key}: must be an array of strings`);
|
|
205
|
+
}
|
|
206
|
+
const entities = isStringArray(redaction[key]) ? redaction[key] : [];
|
|
207
|
+
for (const entity of entities) {
|
|
208
|
+
checkEnum(
|
|
209
|
+
entity,
|
|
210
|
+
REDACTION_ENTITIES,
|
|
211
|
+
`sensitivity.redaction.${key}[]`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
128
217
|
}
|
|
129
218
|
}
|
|
130
219
|
if (config.documentation !== undefined) {
|
|
@@ -35,10 +35,54 @@
|
|
|
35
35
|
},
|
|
36
36
|
"sensitivity": {
|
|
37
37
|
"type": "object",
|
|
38
|
-
"additionalProperties":
|
|
38
|
+
"additionalProperties": false,
|
|
39
39
|
"properties": {
|
|
40
40
|
"enabled": { "type": "boolean" },
|
|
41
|
-
"default": { "enum": ["public", "internal", "confidential", "restricted"] }
|
|
41
|
+
"default": { "enum": ["public", "internal", "confidential", "restricted"] },
|
|
42
|
+
"redaction": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"additionalProperties": false,
|
|
45
|
+
"properties": {
|
|
46
|
+
"enabled": { "type": "boolean" },
|
|
47
|
+
"scanners": {
|
|
48
|
+
"type": "array",
|
|
49
|
+
"items": { "enum": ["builtin", "gitleaks", "presidio"] },
|
|
50
|
+
"minItems": 1
|
|
51
|
+
},
|
|
52
|
+
"failClosed": { "type": "boolean" },
|
|
53
|
+
"requireReview": { "type": "boolean" },
|
|
54
|
+
"allowedEntities": {
|
|
55
|
+
"type": "array",
|
|
56
|
+
"items": {
|
|
57
|
+
"enum": [
|
|
58
|
+
"api_key",
|
|
59
|
+
"bank_account",
|
|
60
|
+
"credit_card",
|
|
61
|
+
"oauth_token",
|
|
62
|
+
"password",
|
|
63
|
+
"private_key",
|
|
64
|
+
"routing_number",
|
|
65
|
+
"ssn"
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"blockedEntities": {
|
|
70
|
+
"type": "array",
|
|
71
|
+
"items": {
|
|
72
|
+
"enum": [
|
|
73
|
+
"api_key",
|
|
74
|
+
"bank_account",
|
|
75
|
+
"credit_card",
|
|
76
|
+
"oauth_token",
|
|
77
|
+
"password",
|
|
78
|
+
"private_key",
|
|
79
|
+
"routing_number",
|
|
80
|
+
"ssn"
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
42
86
|
}
|
|
43
87
|
},
|
|
44
88
|
"sourceRetention": { "enum": ["raw-ok", "sanitized-note-only", "metadata-only", "external-pointer-only"] },
|
|
@@ -15,6 +15,7 @@ import { fileURLToPath } from "node:url";
|
|
|
15
15
|
import {
|
|
16
16
|
loadConfig,
|
|
17
17
|
loadStructure,
|
|
18
|
+
readJsonSafe,
|
|
18
19
|
pluginRootFrom,
|
|
19
20
|
walkFiles,
|
|
20
21
|
parseFrontmatter,
|
|
@@ -60,6 +61,14 @@ const rel = p => path.relative(process.cwd(), p);
|
|
|
60
61
|
const wrel = p => path.relative(wikiRoot, p);
|
|
61
62
|
const categories = config?.categories ?? structure.categoryDirs?.default ?? [];
|
|
62
63
|
const frontmatterRequired = config?.frontmatter !== false;
|
|
64
|
+
const configPath = path.resolve(
|
|
65
|
+
opt("--config") ?? "wiki/lisa-wiki.config.json"
|
|
66
|
+
);
|
|
67
|
+
const localConfigPath = path.join(
|
|
68
|
+
path.dirname(configPath),
|
|
69
|
+
"lisa-wiki.config.local.json"
|
|
70
|
+
);
|
|
71
|
+
const localConfig = readJsonSafe(localConfigPath);
|
|
63
72
|
|
|
64
73
|
const allMd = walkFiles(wikiRoot, { ext: ".md" });
|
|
65
74
|
const allFiles = walkFiles(wikiRoot);
|
|
@@ -71,6 +80,134 @@ const isUnder = (p, dir) => {
|
|
|
71
80
|
const isSynthesisPage = p => categories.some(c => isUnder(p, c));
|
|
72
81
|
const isSourceNote = p => isUnder(p, "sources");
|
|
73
82
|
|
|
83
|
+
// --- 0. redaction policy readiness ----------------------------------------
|
|
84
|
+
const availableRedactionScanners = new Set(["builtin"]);
|
|
85
|
+
const committedRedaction = config?.sensitivity?.redaction;
|
|
86
|
+
const localRedaction = localConfig?.sensitivity?.redaction;
|
|
87
|
+
const addUnsafeLocalOverride = message =>
|
|
88
|
+
report.add(
|
|
89
|
+
"redaction-policy",
|
|
90
|
+
"unsafe-local-override",
|
|
91
|
+
"FAIL",
|
|
92
|
+
message,
|
|
93
|
+
path.relative(wikiRoot, localConfigPath)
|
|
94
|
+
);
|
|
95
|
+
const isArraySubset = (candidate, baseline) =>
|
|
96
|
+
candidate.every(value => baseline.includes(value));
|
|
97
|
+
if (committedRedaction?.enabled === true) {
|
|
98
|
+
const scanners = Array.isArray(committedRedaction.scanners)
|
|
99
|
+
? committedRedaction.scanners
|
|
100
|
+
: [];
|
|
101
|
+
const missingScanners = scanners.filter(
|
|
102
|
+
scanner => !availableRedactionScanners.has(scanner)
|
|
103
|
+
);
|
|
104
|
+
if (scanners.length === 0) {
|
|
105
|
+
report.add(
|
|
106
|
+
"redaction-policy",
|
|
107
|
+
"scanner-missing",
|
|
108
|
+
committedRedaction.failClosed === false ? "WARN" : "FAIL",
|
|
109
|
+
"redaction is enabled but no scanner is selected"
|
|
110
|
+
);
|
|
111
|
+
} else if (missingScanners.length > 0) {
|
|
112
|
+
report.add(
|
|
113
|
+
"redaction-policy",
|
|
114
|
+
"scanner-unavailable",
|
|
115
|
+
committedRedaction.failClosed === false ? "WARN" : "FAIL",
|
|
116
|
+
`redaction scanner unavailable: ${missingScanners.join(", ")}`
|
|
117
|
+
);
|
|
118
|
+
} else {
|
|
119
|
+
report.add(
|
|
120
|
+
"redaction-policy",
|
|
121
|
+
"scanner-available",
|
|
122
|
+
"PASS",
|
|
123
|
+
`redaction scanner available: ${scanners.join(", ")}`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (committedRedaction.failClosed !== true) {
|
|
127
|
+
report.add(
|
|
128
|
+
"redaction-policy",
|
|
129
|
+
"fail-closed",
|
|
130
|
+
"WARN",
|
|
131
|
+
"redaction is enabled without failClosed=true"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (committedRedaction.requireReview !== true) {
|
|
135
|
+
report.add(
|
|
136
|
+
"redaction-policy",
|
|
137
|
+
"review-required",
|
|
138
|
+
"WARN",
|
|
139
|
+
"redaction is enabled without requireReview=true"
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (localRedaction?.enabled === false) {
|
|
143
|
+
addUnsafeLocalOverride("local config disables committed redaction policy");
|
|
144
|
+
}
|
|
145
|
+
if (
|
|
146
|
+
committedRedaction.failClosed === true &&
|
|
147
|
+
localRedaction?.failClosed === false
|
|
148
|
+
) {
|
|
149
|
+
addUnsafeLocalOverride(
|
|
150
|
+
"local config disables committed redaction fail-closed policy"
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (
|
|
154
|
+
committedRedaction.requireReview === true &&
|
|
155
|
+
localRedaction?.requireReview === false
|
|
156
|
+
) {
|
|
157
|
+
addUnsafeLocalOverride(
|
|
158
|
+
"local config disables committed redaction review requirement"
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (localRedaction && Array.isArray(localRedaction.scanners)) {
|
|
162
|
+
const localScanners = localRedaction.scanners;
|
|
163
|
+
const unavailableLocalScanners = localScanners.filter(
|
|
164
|
+
scanner => !availableRedactionScanners.has(scanner)
|
|
165
|
+
);
|
|
166
|
+
const removedScanners = scanners.filter(
|
|
167
|
+
scanner => !localScanners.includes(scanner)
|
|
168
|
+
);
|
|
169
|
+
if (scanners.length > 0 && localScanners.length === 0) {
|
|
170
|
+
addUnsafeLocalOverride(
|
|
171
|
+
"local config removes committed redaction scanners"
|
|
172
|
+
);
|
|
173
|
+
} else if (unavailableLocalScanners.length > 0) {
|
|
174
|
+
addUnsafeLocalOverride(
|
|
175
|
+
`local config selects unavailable redaction scanner: ${unavailableLocalScanners.join(", ")}`
|
|
176
|
+
);
|
|
177
|
+
} else if (removedScanners.length > 0) {
|
|
178
|
+
addUnsafeLocalOverride(
|
|
179
|
+
`local config removes committed redaction scanner: ${removedScanners.join(", ")}`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (
|
|
184
|
+
localRedaction &&
|
|
185
|
+
Array.isArray(committedRedaction.allowedEntities) &&
|
|
186
|
+
Array.isArray(localRedaction.allowedEntities) &&
|
|
187
|
+
!isArraySubset(
|
|
188
|
+
localRedaction.allowedEntities,
|
|
189
|
+
committedRedaction.allowedEntities
|
|
190
|
+
)
|
|
191
|
+
) {
|
|
192
|
+
addUnsafeLocalOverride(
|
|
193
|
+
"local config expands committed redaction allowed entities"
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (
|
|
197
|
+
localRedaction &&
|
|
198
|
+
Array.isArray(committedRedaction.blockedEntities) &&
|
|
199
|
+
Array.isArray(localRedaction.blockedEntities) &&
|
|
200
|
+
!isArraySubset(
|
|
201
|
+
committedRedaction.blockedEntities,
|
|
202
|
+
localRedaction.blockedEntities
|
|
203
|
+
)
|
|
204
|
+
) {
|
|
205
|
+
addUnsafeLocalOverride(
|
|
206
|
+
"local config removes committed redaction blocked entities"
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
74
211
|
// --- A. structure conformance ---------------------------------------------
|
|
75
212
|
for (const f of structure.requiredFiles ?? []) {
|
|
76
213
|
report.add(
|