yam-flow 2.8.0 → 2.8.2
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/AGENTS.md +4 -0
- package/CHANGELOG.md +22 -0
- package/COMMANDS.md +17 -2
- package/README.md +5 -1
- package/dist/bin/yam.js +162 -42
- package/dist/lib/design-production.d.ts +6 -6
- package/dist/lib/external-updates.d.ts +6 -1
- package/dist/lib/external-updates.js +93 -6
- package/dist/lib/hook-readiness.d.ts +54 -0
- package/dist/lib/hook-readiness.js +259 -0
- package/dist/lib/memory-records.d.ts +29 -0
- package/dist/lib/memory-records.js +92 -0
- package/dist/lib/security-check.d.ts +98 -0
- package/dist/lib/security-check.js +262 -0
- package/dist/lib/security-integration.d.ts +8 -0
- package/dist/lib/security-integration.js +428 -0
- package/dist/lib/verification-closure.d.ts +1 -1
- package/package.json +10 -3
- package/references/final-report.md +2 -0
- package/references/security-check.md +131 -0
- package/references/study-note.md +6 -0
- package/skills/deep/SKILL.md +4 -0
- package/skills/mission/SKILL.md +4 -0
- package/skills/quick/SKILL.md +4 -0
- package/skills/ueye/SKILL.md +4 -0
- package/src/bin/yam.ts +142 -39
- package/src/lib/external-updates.ts +87 -6
- package/src/lib/hook-readiness.ts +215 -0
- package/src/lib/memory-records.ts +109 -0
- package/src/lib/security-check.ts +184 -0
- package/src/lib/security-integration.ts +299 -0
- package/templates/security-check-note.md +42 -0
- package/templates/security-gate.yml +44 -0
- package/yam.manifest.json +1 -1
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { constants, readFileSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { createSecurityStarter, validateSecurityCheck, persistSecurityCheck, SECURITY_AREA_IDS } from './security-check.js';
|
|
8
|
+
const OID = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
|
|
9
|
+
const DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
10
|
+
const ZERO = /^0+$/;
|
|
11
|
+
const hash = (text) => `sha256:${createHash('sha256').update(text).digest('hex')}`;
|
|
12
|
+
function git(cwd, args) {
|
|
13
|
+
try {
|
|
14
|
+
return execFileSync('git', ['-C', cwd, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 8 * 1024 * 1024 }).trim();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error('git binding inspection failed');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function revision(cwd, ref) {
|
|
21
|
+
if (!OID.test(ref))
|
|
22
|
+
throw new Error('revision must be an exact Git object ID');
|
|
23
|
+
const result = git(cwd, ['rev-parse', '--verify', `${ref}^{commit}`]);
|
|
24
|
+
if (!OID.test(result))
|
|
25
|
+
throw new Error('invalid source commit');
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
function parseArgs(args) {
|
|
29
|
+
const out = new Map();
|
|
30
|
+
for (let i = 0; i < args.length; i++) {
|
|
31
|
+
if (args[i] === '--json')
|
|
32
|
+
continue;
|
|
33
|
+
if (!['--policy', '--policy-digest', '--evidence', '--note', '--stage', '--remote', '--destination', '--source', '--base', '--artifact', '--artifact-digest', '--configuration', '--configuration-digest', '--environment', '--cli'].includes(args[i]) || !args[i + 1] || args[i + 1].startsWith('--') || out.has(args[i]))
|
|
34
|
+
throw new Error('invalid or duplicate security option');
|
|
35
|
+
out.set(args[i], args[++i]);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
export function collectSecurityBinding(cwd, stage, options, stdin = '') {
|
|
40
|
+
if (stage === 'commit') {
|
|
41
|
+
let base;
|
|
42
|
+
try {
|
|
43
|
+
base = git(cwd, ['rev-parse', '--verify', 'HEAD']);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
base = execFileSync('git', ['-C', cwd, 'hash-object', '-t', 'tree', '--stdin'], { input: '', encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
47
|
+
}
|
|
48
|
+
return { stage, staged_tree: git(cwd, ['write-tree']), base_revision: base };
|
|
49
|
+
}
|
|
50
|
+
if (stage === 'push') {
|
|
51
|
+
const remote = options.get('--remote');
|
|
52
|
+
if (!remote || /[\r\n\0]/.test(remote))
|
|
53
|
+
throw new Error('push requires remote identity');
|
|
54
|
+
const destination = options.get('--destination');
|
|
55
|
+
if (!destination || /[\r\n\0]/.test(destination))
|
|
56
|
+
throw new Error('push requires actual Git hook destination');
|
|
57
|
+
const lines = stdin.trim().split('\n').filter(Boolean);
|
|
58
|
+
if (!lines.length || lines.length > 256)
|
|
59
|
+
throw new Error('push requires exact outgoing refs from hook stdin');
|
|
60
|
+
const push_refs = lines.map(line => {
|
|
61
|
+
const fields = line.trim().split(/\s+/);
|
|
62
|
+
if (fields.length !== 4 || !OID.test(fields[1]) || !OID.test(fields[3]) || ZERO.test(fields[1]) || !(fields[0] === 'HEAD' || fields[0].startsWith('refs/')) || !fields[2].startsWith('refs/'))
|
|
63
|
+
throw new Error('invalid push ref or deletion; deletion requires separate reviewed handling');
|
|
64
|
+
revision(cwd, fields[1]);
|
|
65
|
+
return { local_ref: fields[0], local_oid: fields[1], remote_ref: fields[2], remote_oid: fields[3] };
|
|
66
|
+
});
|
|
67
|
+
if (new Set(push_refs.map(r => r.remote_ref)).size !== push_refs.length)
|
|
68
|
+
throw new Error('duplicate outgoing destination ref');
|
|
69
|
+
return { stage, destination_digest: hash(destination), push_refs };
|
|
70
|
+
}
|
|
71
|
+
if (stage === 'ci') {
|
|
72
|
+
const source = revision(cwd, options.get('--source') || '');
|
|
73
|
+
const base = revision(cwd, options.get('--base') || '');
|
|
74
|
+
return { stage: 'push', context: 'ci', source_revision: source, base_revision: base, tree: git(cwd, ['rev-parse', `${source}^{tree}`]) };
|
|
75
|
+
}
|
|
76
|
+
if (stage === 'deploy') {
|
|
77
|
+
const source = revision(cwd, options.get('--source') || '');
|
|
78
|
+
const artifact = options.get('--artifact-digest') || '';
|
|
79
|
+
const configuration = options.get('--configuration-digest') || '';
|
|
80
|
+
const environment = options.get('--environment') || '';
|
|
81
|
+
if (!DIGEST.test(artifact) || !DIGEST.test(configuration) || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,95}$/.test(environment))
|
|
82
|
+
throw new Error('deploy requires exact artifact/configuration SHA256 and environment');
|
|
83
|
+
return { stage, source_revision: source, artifact_digest: artifact, configuration_digest: configuration, environment };
|
|
84
|
+
}
|
|
85
|
+
throw new Error('stage must be commit, push, ci or deploy');
|
|
86
|
+
}
|
|
87
|
+
async function readSafe(file) {
|
|
88
|
+
await safeParents(file);
|
|
89
|
+
const handle = await fs.open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
90
|
+
try {
|
|
91
|
+
const info = await handle.stat();
|
|
92
|
+
if (!info.isFile() || info.size > 1024 * 1024)
|
|
93
|
+
throw new Error('invalid security file');
|
|
94
|
+
return await handle.readFile('utf8');
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
await handle.close();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function quote(value) { return `'${value.replace(/'/g, `'\\''`)}'`; }
|
|
101
|
+
async function exclusive(file, text, mode = 0o600) {
|
|
102
|
+
await safeParents(file);
|
|
103
|
+
const handle = await fs.open(file, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, mode);
|
|
104
|
+
try {
|
|
105
|
+
await handle.writeFile(text, 'utf8');
|
|
106
|
+
await handle.sync();
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
await handle.close();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function gitMetadata(cwd) {
|
|
113
|
+
const root = git(cwd, ['rev-parse', '--show-toplevel']);
|
|
114
|
+
const directory = path.resolve(cwd, git(cwd, ['rev-parse', '--git-dir']));
|
|
115
|
+
const hookOverride = (() => { try {
|
|
116
|
+
return git(cwd, ['config', '--get', 'core.hooksPath']);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return '';
|
|
120
|
+
} })();
|
|
121
|
+
if (hookOverride)
|
|
122
|
+
throw new Error('custom hooksPath requires explicit manual integration');
|
|
123
|
+
return { root, directory, hooks: path.join(directory, 'hooks'), config: path.join(directory, 'yam-security.json') };
|
|
124
|
+
}
|
|
125
|
+
async function enableHooks(cwd, options, cliPath) {
|
|
126
|
+
if (process.platform === 'win32')
|
|
127
|
+
throw new Error('automatic hook setup requires a POSIX Git shell; integrate manually on Windows');
|
|
128
|
+
const metadata = await gitMetadata(cwd);
|
|
129
|
+
const settings = JSON.parse(await readSafe(metadata.config));
|
|
130
|
+
if (settings.schema !== 'yam.security-install.v1' || !DIGEST.test(settings.policy_digest))
|
|
131
|
+
throw new Error('security init required before hooks enable');
|
|
132
|
+
const cli = await fs.realpath(cliPath || options.get('--cli') || fileURLToPath(new URL('../bin/yam.js', import.meta.url)));
|
|
133
|
+
const node = await fs.realpath(process.execPath);
|
|
134
|
+
if (!(await fs.stat(cli)).isFile())
|
|
135
|
+
throw new Error('built CLI path required');
|
|
136
|
+
const files = ['pre-commit', 'pre-push'].map(name => path.join(metadata.hooks, name));
|
|
137
|
+
for (const file of files) {
|
|
138
|
+
try {
|
|
139
|
+
await fs.lstat(file);
|
|
140
|
+
throw new Error('existing Git hooks preserved; use manual integration');
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (error.code !== 'ENOENT')
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
await fs.mkdir(metadata.hooks, { recursive: true });
|
|
148
|
+
const common = `${quote(node)} ${quote(cli)} security check --policy ${quote(settings.policy)} --policy-digest ${quote(settings.policy_digest)} --evidence ${quote(settings.evidence)} --note ${quote(settings.note)}`;
|
|
149
|
+
const created = [];
|
|
150
|
+
try {
|
|
151
|
+
for (const [index, file] of files.entries()) {
|
|
152
|
+
const stage = index === 0 ? 'commit' : 'push';
|
|
153
|
+
const remote = index === 1 ? ' --remote "$1" --destination "$2"' : '';
|
|
154
|
+
await exclusive(file, `#!/bin/sh\n# yam.security-hook.v1\nexec ${common} --stage ${stage}${remote}\n`, 0o700);
|
|
155
|
+
created.push(file);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
for (const file of created)
|
|
160
|
+
await fs.unlink(file);
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
return { ok: true, result: 'ENABLED', hooks: ['pre-commit', 'pre-push'], enforcement: 'local_bypassable', ci_required: true };
|
|
164
|
+
}
|
|
165
|
+
async function safeParents(file) {
|
|
166
|
+
let current = path.dirname(path.resolve(file));
|
|
167
|
+
while (current !== path.dirname(current)) {
|
|
168
|
+
try {
|
|
169
|
+
const info = await fs.lstat(current);
|
|
170
|
+
if (info.isSymbolicLink() || !info.isDirectory())
|
|
171
|
+
throw new Error('unsafe security file parent');
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (error.code !== 'ENOENT')
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
current = path.dirname(current);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function initialize(cwd, options) {
|
|
181
|
+
const metadata = await gitMetadata(cwd);
|
|
182
|
+
const base = path.join(metadata.root, '.yam', 'security');
|
|
183
|
+
const policyPath = path.resolve(metadata.root, options.get('--policy') || '.yam/security/policy.json');
|
|
184
|
+
const evidencePath = path.resolve(metadata.root, options.get('--evidence') || '.yam/security/check.json');
|
|
185
|
+
const notePath = path.resolve(metadata.root, options.get('--note') || '.yam/security/notes.md');
|
|
186
|
+
for (const target of [policyPath, evidencePath, notePath]) {
|
|
187
|
+
if (!target.startsWith(metadata.root + path.sep))
|
|
188
|
+
throw new Error('init files must be inside repository');
|
|
189
|
+
await safeParents(target);
|
|
190
|
+
}
|
|
191
|
+
// All areas have explicit trusted requirements; live environment inspection is deferred to deploy.
|
|
192
|
+
const policy = { version: 'security-check-v1', required_at: Object.fromEntries(SECURITY_AREA_IDS.map(id => [id, id === 'SEC-05' ? ['deploy'] : ['commit', 'push', 'deploy']])) };
|
|
193
|
+
const policyText = JSON.stringify(policy, null, 2) + '\n';
|
|
194
|
+
const binding = collectSecurityBinding(metadata.root, 'commit', options);
|
|
195
|
+
const starter = createSecurityStarter({ stage: 'commit', policy, binding: binding, environment: 'local', scope: ['initial project security assessment'], checker: 'operator-review-required' });
|
|
196
|
+
starter.policy_digest = hash(policyText);
|
|
197
|
+
const files = [policyPath, evidencePath, metadata.config];
|
|
198
|
+
for (const file of files) {
|
|
199
|
+
try {
|
|
200
|
+
await fs.lstat(file);
|
|
201
|
+
throw new Error('security init preserves existing files');
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (error.code !== 'ENOENT')
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
await safeParents(path.join(base, 'placeholder'));
|
|
209
|
+
await fs.mkdir(base, { recursive: true });
|
|
210
|
+
const created = [];
|
|
211
|
+
try {
|
|
212
|
+
for (const [file, content] of [
|
|
213
|
+
[policyPath, policyText], [evidencePath, JSON.stringify(starter, null, 2) + '\n'],
|
|
214
|
+
[metadata.config, JSON.stringify({ schema: 'yam.security-install.v1', policy: policyPath, policy_digest: hash(policyText), evidence: evidencePath, note: notePath }, null, 2) + '\n']
|
|
215
|
+
]) {
|
|
216
|
+
await exclusive(file, content);
|
|
217
|
+
created.push(file);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
for (const file of created)
|
|
222
|
+
await fs.unlink(file);
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
return { ok: true, result: 'INITIALIZED', policy_digest: hash(policyText), evidence: path.relative(metadata.root, evidencePath), hooks_installed: false, evidence_boundary: 'operator_supplied_not_automatic_security_scan' };
|
|
226
|
+
}
|
|
227
|
+
async function verifyReferencedEvidence(cwd, input) {
|
|
228
|
+
const errors = [];
|
|
229
|
+
for (const area of input.areas || []) {
|
|
230
|
+
if (!SECURITY_AREA_IDS.includes(area.id)) {
|
|
231
|
+
errors.push('invalid_evidence_area');
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (area.status === 'passed' && !area.evidence?.length)
|
|
235
|
+
errors.push(`evidence_missing:${area.id}`);
|
|
236
|
+
for (const evidence of area.evidence || []) {
|
|
237
|
+
if (typeof evidence.reference !== 'string' || !evidence.reference || path.isAbsolute(evidence.reference) || /^[a-z]+:/i.test(evidence.reference)) {
|
|
238
|
+
errors.push(`evidence_reference_invalid:${area.id}`);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const file = path.resolve(cwd, evidence.reference);
|
|
242
|
+
if (!file.startsWith(path.resolve(cwd) + path.sep)) {
|
|
243
|
+
errors.push(`evidence_outside_repository:${area.id}`);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
if (await digestFile(file, 16 * 1024 * 1024) !== evidence.sha256)
|
|
248
|
+
errors.push(`evidence_digest_mismatch:${area.id}`);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
errors.push(`evidence_unavailable:${area.id}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return errors;
|
|
256
|
+
}
|
|
257
|
+
export async function runSecurityCommand(args, settings = {}) {
|
|
258
|
+
const cwd = await fs.realpath(path.resolve(settings.cwd || process.cwd()));
|
|
259
|
+
const subcommand = args[0];
|
|
260
|
+
try {
|
|
261
|
+
if (subcommand === 'hooks') {
|
|
262
|
+
if (args[1] !== 'enable')
|
|
263
|
+
throw new Error('supported hook operation: enable');
|
|
264
|
+
return await enableHooks(cwd, parseArgs(args.slice(2)), settings.cliPath);
|
|
265
|
+
}
|
|
266
|
+
const options = parseArgs(args.slice(1));
|
|
267
|
+
if (subcommand === 'init')
|
|
268
|
+
return await initialize(cwd, options);
|
|
269
|
+
if (subcommand === 'status') {
|
|
270
|
+
const metadata = await gitMetadata(cwd);
|
|
271
|
+
let configured = false;
|
|
272
|
+
try {
|
|
273
|
+
const config = JSON.parse(await readSafe(metadata.config));
|
|
274
|
+
configured = config.schema === 'yam.security-install.v1';
|
|
275
|
+
}
|
|
276
|
+
catch { /* unavailable is reported */ }
|
|
277
|
+
const installed = [];
|
|
278
|
+
for (const name of ['pre-commit', 'pre-push']) {
|
|
279
|
+
try {
|
|
280
|
+
if ((await readSafe(path.join(metadata.hooks, name))).includes('# yam.security-hook.v1'))
|
|
281
|
+
installed.push(name);
|
|
282
|
+
}
|
|
283
|
+
catch { /* unavailable is reported */ }
|
|
284
|
+
}
|
|
285
|
+
return { ok: true, configured, local_hooks: installed, local_hooks_bypassable: true, remote_enforcement: 'not_measured', evidence_boundary: 'operator_supplied_not_automatic_security_scan' };
|
|
286
|
+
}
|
|
287
|
+
if (subcommand !== 'check')
|
|
288
|
+
throw new Error('supported commands: init, check, hooks enable, status');
|
|
289
|
+
return await check(cwd, options, settings);
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
if (subcommand === 'check') {
|
|
293
|
+
const input = createSecurityStarter({ stage: 'commit', binding: { stage: 'commit', invalid_binding: true }, environment: 'local', scope: ['rejected security command'], checker: 'yam-security-check' });
|
|
294
|
+
const result = validateSecurityCheck(input);
|
|
295
|
+
result.errors.push('security_command_failed');
|
|
296
|
+
result.blocking.push('integration:security_command_failed');
|
|
297
|
+
result.result = 'BLOCKED';
|
|
298
|
+
try {
|
|
299
|
+
const persisted = await persistSecurityCheck(cwd, input, result);
|
|
300
|
+
return { ok: false, ...persisted, evidence_boundary: 'operator_supplied_not_automatic_security_scan' };
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return { ok: false, result: 'BLOCKED', errors: ['security_command_failed', 'security_note_persistence_failed'] };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return { ok: false, result: 'BLOCKED', errors: ['security_command_failed'], evidence_boundary: 'operator_supplied_not_automatic_security_scan' };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async function check(cwd, options, settings) {
|
|
310
|
+
const stage = options.get('--stage') || 'commit';
|
|
311
|
+
const normalizedStage = stage === 'ci' ? 'push' : stage;
|
|
312
|
+
let config = {};
|
|
313
|
+
try {
|
|
314
|
+
const metadata = await gitMetadata(cwd);
|
|
315
|
+
config = JSON.parse(await readSafe(metadata.config));
|
|
316
|
+
}
|
|
317
|
+
catch { /* an explicit trust pin can be provided instead */ }
|
|
318
|
+
const notePath = path.resolve(cwd, options.get('--note') || config.note || '.yam/security/notes.md');
|
|
319
|
+
let input;
|
|
320
|
+
let binding = { stage: normalizedStage, invalid_binding: true };
|
|
321
|
+
let policy = { version: 'security-check-v1', required_at: Object.fromEntries(SECURITY_AREA_IDS.map(id => [id, ['commit', 'push', 'deploy']])) };
|
|
322
|
+
let result;
|
|
323
|
+
const gateErrors = [];
|
|
324
|
+
try {
|
|
325
|
+
const policyFile = path.resolve(cwd, options.get('--policy') || config.policy || '.yam/security/policy.json');
|
|
326
|
+
const pin = options.get('--policy-digest') || ((stage !== 'ci' && stage !== 'deploy') ? config.policy_digest : undefined);
|
|
327
|
+
if (!pin || !DIGEST.test(pin))
|
|
328
|
+
throw new Error('policy_pin_missing');
|
|
329
|
+
const policyText = await readSafe(policyFile);
|
|
330
|
+
if (hash(policyText) !== pin)
|
|
331
|
+
throw new Error('policy_pin_mismatch');
|
|
332
|
+
policy = JSON.parse(policyText);
|
|
333
|
+
if (!policy || typeof policy.version !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(policy.version) || Object.keys(policy).some(key => !['version', 'required_at'].includes(key)) || !policy.required_at || Object.keys(policy.required_at).some(key => !SECURITY_AREA_IDS.includes(key)) || SECURITY_AREA_IDS.some(id => !Array.isArray(policy.required_at[id])))
|
|
334
|
+
throw new Error('policy_invalid');
|
|
335
|
+
for (const id of ['SEC-01', 'SEC-08'])
|
|
336
|
+
if (!['commit', 'push', 'deploy'].every(s => policy.required_at[id].includes(s)))
|
|
337
|
+
throw new Error('mandatory_policy_invalid');
|
|
338
|
+
for (const id of SECURITY_AREA_IDS)
|
|
339
|
+
if (!policy.required_at[id].length || policy.required_at[id].some(s => !['commit', 'push', 'deploy'].includes(s)))
|
|
340
|
+
throw new Error('policy_stage_invalid');
|
|
341
|
+
const stdin = stage === 'push' ? (settings.stdin ?? await readSafeStdin()) : '';
|
|
342
|
+
if (stage === 'deploy') {
|
|
343
|
+
for (const [fileOption, digestOption] of [['--artifact', '--artifact-digest'], ['--configuration', '--configuration-digest']]) {
|
|
344
|
+
const file = options.get(fileOption);
|
|
345
|
+
if (!file || await digestFile(path.resolve(cwd, file), 1024 * 1024 * 1024) !== options.get(digestOption))
|
|
346
|
+
throw new Error('deployment_file_digest_mismatch');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
binding = collectSecurityBinding(cwd, stage, options, stdin);
|
|
350
|
+
const evidenceText = await readSafe(path.resolve(cwd, options.get('--evidence') || config.evidence || '.yam/security/check.json'));
|
|
351
|
+
input = JSON.parse(evidenceText);
|
|
352
|
+
if (input.policy_digest !== pin)
|
|
353
|
+
gateErrors.push('policy_digest_mismatch');
|
|
354
|
+
if (input.stage !== normalizedStage)
|
|
355
|
+
gateErrors.push('stage_mismatch');
|
|
356
|
+
if (SECURITY_AREA_IDS.some(id => !input.areas?.some(area => area.id === id)))
|
|
357
|
+
gateErrors.push('area_coverage_missing');
|
|
358
|
+
if (input.areas?.some(area => ['SEC-01', 'SEC-08'].includes(area.id) && area.status === 'not_applicable'))
|
|
359
|
+
gateErrors.push('mandatory_area_not_applicable');
|
|
360
|
+
gateErrors.push(...await verifyReferencedEvidence(cwd, input));
|
|
361
|
+
input = { ...input, retry_of: input.run_id, run_id: `security-${randomUUID()}`, time: new Date().toISOString() };
|
|
362
|
+
result = validateSecurityCheck(input, binding, policy);
|
|
363
|
+
if (result.errors.includes('record_contains_secret')) {
|
|
364
|
+
input = createSecurityStarter({ stage: normalizedStage, binding: binding, environment: 'redacted', scope: ['unsafe supplied record rejected'], checker: 'yam-security-check' });
|
|
365
|
+
result = validateSecurityCheck(input, binding);
|
|
366
|
+
gateErrors.push('unsafe_security_record_rejected');
|
|
367
|
+
}
|
|
368
|
+
// Inspect the index/refs once more before persisting so mid-check changes cannot retain PASS.
|
|
369
|
+
const fresh = collectSecurityBinding(cwd, stage, options, stdin);
|
|
370
|
+
if (JSON.stringify(fresh) !== JSON.stringify(binding))
|
|
371
|
+
gateErrors.push('binding_changed_during_check');
|
|
372
|
+
if (stage === 'deploy') {
|
|
373
|
+
for (const [fileOption, digestOption] of [['--artifact', '--artifact-digest'], ['--configuration', '--configuration-digest']]) {
|
|
374
|
+
if (await digestFile(path.resolve(cwd, options.get(fileOption)), 1024 * 1024 * 1024) !== options.get(digestOption))
|
|
375
|
+
gateErrors.push('deployment_file_changed_during_check');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
input = createSecurityStarter({ stage: normalizedStage, binding: binding, environment: stage === 'deploy' ? options.get('--environment') || 'unavailable' : 'local', scope: ['security gate unavailable'], checker: 'yam-security-check' });
|
|
381
|
+
result = validateSecurityCheck(input, binding);
|
|
382
|
+
gateErrors.push('security_input_or_binding_unavailable');
|
|
383
|
+
}
|
|
384
|
+
if (gateErrors.length) {
|
|
385
|
+
result.result = 'BLOCKED';
|
|
386
|
+
result.errors = [...new Set([...result.errors, ...gateErrors])];
|
|
387
|
+
result.blocking = [...new Set([...result.blocking, ...gateErrors.map(e => `integration:${e}`)])];
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
const persisted = await persistSecurityCheck(cwd, input, result, { notePath });
|
|
391
|
+
return { ok: persisted.result.result === 'PASS', ...persisted, evidence_boundary: 'operator_supplied_not_automatic_security_scan' };
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
return { ok: false, result: 'BLOCKED', errors: ['security_note_persistence_failed'], evidence_boundary: 'operator_supplied_not_automatic_security_scan' };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async function readSafeStdin() {
|
|
398
|
+
const text = readFileSync(0, 'utf8');
|
|
399
|
+
if (text.length > 128 * 1024)
|
|
400
|
+
throw new Error('push_ref_input_too_large');
|
|
401
|
+
return text;
|
|
402
|
+
}
|
|
403
|
+
async function digestFile(file, limit) {
|
|
404
|
+
await safeParents(file);
|
|
405
|
+
const handle = await fs.open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
406
|
+
try {
|
|
407
|
+
const info = await handle.stat();
|
|
408
|
+
if (!info.isFile() || info.size > limit)
|
|
409
|
+
throw new Error('invalid digest target');
|
|
410
|
+
const digest = createHash('sha256');
|
|
411
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
412
|
+
let position = 0;
|
|
413
|
+
while (position < info.size) {
|
|
414
|
+
const { bytesRead } = await handle.read(buffer, 0, Math.min(buffer.length, info.size - position), position);
|
|
415
|
+
if (!bytesRead)
|
|
416
|
+
throw new Error('digest target changed during read');
|
|
417
|
+
digest.update(buffer.subarray(0, bytesRead));
|
|
418
|
+
position += bytesRead;
|
|
419
|
+
}
|
|
420
|
+
const after = await handle.stat();
|
|
421
|
+
if (after.size !== info.size || after.mtimeMs !== info.mtimeMs || after.ctimeMs !== info.ctimeMs)
|
|
422
|
+
throw new Error('digest target changed during read');
|
|
423
|
+
return `sha256:${digest.digest('hex')}`;
|
|
424
|
+
}
|
|
425
|
+
finally {
|
|
426
|
+
await handle.close();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
@@ -63,7 +63,7 @@ export declare function createVerificationClosureReceipt(input: {
|
|
|
63
63
|
schema: "yam.verification-closure-write.v1";
|
|
64
64
|
receipt_path: string;
|
|
65
65
|
receipt: VerificationClosureReceipt;
|
|
66
|
-
truth_status: "
|
|
66
|
+
truth_status: "partial" | "blocked";
|
|
67
67
|
next_action: string;
|
|
68
68
|
}>;
|
|
69
69
|
export declare function verifyVerificationClosureReceipt(value: unknown): {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yam-flow",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.2",
|
|
4
4
|
"description": "Progressive proof-first agent flow: start fast, deepen deliberately, stay honest by design.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "0kim0bos",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"doctor": "node ./dist/bin/yam.js doctor",
|
|
61
61
|
"list": "node ./dist/bin/yam.js list",
|
|
62
62
|
"examples": "node ./dist/bin/yam.js examples",
|
|
63
|
-
"verify:self": "npm run build && node --check ./dist/bin/yam.js && node --check ./dist/lib/trust-kernel.js && node --check ./dist/lib/skill-installation.js && node --check ./dist/lib/external-updates.js && node --check ./dist/lib/next-step.js && node --check ./dist/lib/scout-receipt.js && node --check ./dist/lib/release-registry-status.js && node --check ./dist/lib/release-auth-readiness.js && node --check ./dist/lib/gate-result.js && node --check ./dist/lib/benchmark-promotion.js && node --check ./dist/lib/design-production.js && node --check ./dist/lib/verification-closure.js && node --check ./dist/lib/media-provider-receipt.js && npm run hook-latency-smoke && npm run next-step-smoke && npm run scout-receipt-smoke && npm run release:registry-parser:smoke && npm run release:auth-smoke && node ./scripts/external-updates-smoke.mjs && node ./scripts/install-transaction-smoke.mjs && node ./scripts/trust-adoptions-smoke.mjs && node ./scripts/design-production-smoke.mjs && node ./scripts/phase-adoptions-smoke.mjs && npm run package-secrets:check && npm run package-secrets-smoke && node ./dist/bin/yam.js verify && node ./dist/bin/yam.js doctor",
|
|
63
|
+
"verify:self": "npm run build && node --check ./dist/bin/yam.js && node --check ./dist/lib/trust-kernel.js && node --check ./dist/lib/skill-installation.js && node --check ./dist/lib/external-updates.js && node --check ./dist/lib/hook-readiness.js && node --check ./dist/lib/memory-records.js && node --check ./dist/lib/next-step.js && node --check ./dist/lib/scout-receipt.js && node --check ./dist/lib/release-registry-status.js && node --check ./dist/lib/release-auth-readiness.js && node --check ./dist/lib/gate-result.js && node --check ./dist/lib/benchmark-promotion.js && node --check ./dist/lib/design-production.js && node --check ./dist/lib/verification-closure.js && node --check ./dist/lib/media-provider-receipt.js && npm run security-smoke && npm run security-integration-smoke && npm run hook-latency-smoke && npm run hook-readiness-smoke && npm run next-step-smoke && npm run scout-receipt-smoke && npm run release:registry-parser:smoke && npm run release:auth-smoke && node ./scripts/external-updates-smoke.mjs && node ./scripts/install-transaction-smoke.mjs && node ./scripts/trust-adoptions-smoke.mjs && node ./scripts/design-production-smoke.mjs && node ./scripts/phase-adoptions-smoke.mjs && npm run skill-content:check && npm run skill-content-smoke && npm run memory-completeness-smoke && npm run package-secrets:check && npm run package-secrets-smoke && node ./dist/bin/yam.js verify && node ./dist/bin/yam.js doctor",
|
|
64
64
|
"prepare": "npm run build",
|
|
65
65
|
"prepack": "npm run verify:self",
|
|
66
66
|
"prepublishOnly": "npm run verify:self",
|
|
@@ -72,6 +72,10 @@
|
|
|
72
72
|
"package-boundary:check": "node ./scripts/check-package-boundary.mjs",
|
|
73
73
|
"package-secrets:check": "node ./scripts/check-package-secrets.mjs",
|
|
74
74
|
"package-secrets-smoke": "node ./scripts/package-secrets-smoke.mjs",
|
|
75
|
+
"skill-content:check": "node ./scripts/check-skill-content.mjs",
|
|
76
|
+
"skill-content-smoke": "node ./scripts/skill-content-smoke.mjs",
|
|
77
|
+
"memory-completeness-smoke": "node ./scripts/memory-completeness-smoke.mjs",
|
|
78
|
+
"hook-readiness-smoke": "node ./scripts/hook-readiness-smoke.mjs",
|
|
75
79
|
"dist:freshness": "node ./scripts/check-dist-freshness.mjs",
|
|
76
80
|
"registry:check": "node ./scripts/check-registry-version.mjs",
|
|
77
81
|
"registry:check:smoke": "node ./scripts/check-registry-version.mjs --self-test",
|
|
@@ -102,7 +106,10 @@
|
|
|
102
106
|
"memory": "node ./dist/bin/yam.js memory",
|
|
103
107
|
"template": "node ./dist/bin/yam.js template",
|
|
104
108
|
"tune-log": "node ./dist/bin/yam.js tune-log",
|
|
105
|
-
"init-project": "node ./dist/bin/yam.js init-project"
|
|
109
|
+
"init-project": "node ./dist/bin/yam.js init-project",
|
|
110
|
+
"security:check": "node ./dist/bin/yam.js security check --stage ci --json",
|
|
111
|
+
"security-smoke": "node ./scripts/security-check-smoke.mjs",
|
|
112
|
+
"security-integration-smoke": "node ./scripts/security-integration-smoke.mjs"
|
|
106
113
|
},
|
|
107
114
|
"keywords": [
|
|
108
115
|
"codex",
|
|
@@ -4,6 +4,8 @@ Every `yam` route should end with a compact handoff that helps the next run avoi
|
|
|
4
4
|
|
|
5
5
|
## Required Closing Check
|
|
6
6
|
|
|
7
|
+
For commit, push, or deployment security reports, use the separate Security check block defined in `references/security-check.md` and `templates/security-check-note.md`. An SEC ID alone is insufficient: include its concrete security explanation, evidence, limitations, and the stage decision. Keep this block before Study Note or after Next step. The CLI validates supplied evidence and writes notes; local hooks and remote CI/deployment enforcement require explicit project setup.
|
|
8
|
+
|
|
7
9
|
Always keep final reports short. Include these when they apply:
|
|
8
10
|
|
|
9
11
|
- What changed or what was found.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Security check contract v1
|
|
2
|
+
|
|
3
|
+
This document defines the security-check contract and the human-readable `Security check` block in a Yam note. `yam security check` validates supplied reports, trusted stage policy, target bindings and evidence-file hashes, then writes a note. It is not an automatic vulnerability scanner and does not change `yam.study-note.v1`. A passing gate proves that the supplied evidence contract matches the checked target; it does not independently establish the truth or completeness of the evidence.
|
|
4
|
+
|
|
5
|
+
## Scope and timing
|
|
6
|
+
|
|
7
|
+
Assess security applicability during new development and changes. Before commit, push, or deployment, require a note and evidence for the checks required at that stage. A new project receives an initial assessment of all nine areas; later work reassesses changed boundaries and may reuse unchanged evidence with its source and validity reason.
|
|
8
|
+
|
|
9
|
+
- Commit: bind the run to the actual staged tree and base revision, including newly staged files. Unstaged working files are not proof of the staged content. Changing the index invalidates the result.
|
|
10
|
+
- Push: bind the run to the exact outgoing commit IDs and destination. Recheck the outgoing changes; a local note alone is insufficient proof for the future remote CI gate.
|
|
11
|
+
- Deploy: bind the run to the exact artifact digest, source commit, target environment, and relevant configuration revision. Verify actual environment settings; build success does not establish deployment security.
|
|
12
|
+
- Evidence reuse is valid only when the checked content, policy version, dependencies, and relevant environment/configuration remain unchanged. A code digest alone cannot validate live environment evidence.
|
|
13
|
+
|
|
14
|
+
Commit, push, and deployment are distinct actions. A remote CI failure generally blocks protected-branch merge or deployment; it does not mean the preceding Git push was rejected. Local hooks can be bypassed or absent. Do not claim that `--no-verify` is automatically observable; future CI must independently check incoming revisions. Direct deployments must use the same required gate or access restrictions, otherwise enforcement remains incomplete.
|
|
15
|
+
|
|
16
|
+
## Fixed areas and concrete summaries
|
|
17
|
+
|
|
18
|
+
Every run includes each ID exactly once with a concrete explanation, a status, and the applicable evidence or reason. An ID or short heading alone does not satisfy the explanation requirement. These summaries are the canonical minimum; add project-specific targets without removing their security meaning.
|
|
19
|
+
|
|
20
|
+
| ID | Required security explanation summary |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| SEC-01 | 사용자·관리자·외부 서비스·DB·파일 사이의 신뢰 경계와 보호할 데이터·작업을 식별한다. |
|
|
23
|
+
| SEC-02 | 명시적으로 허용한 접근만 열고, 사용자·서비스 계정에 필요한 최소 권한만 부여한다. |
|
|
24
|
+
| SEC-03 | 화면을 우회한 API·서버 함수·DB·파일·백그라운드 작업에서도 사용자·조직·대상별 권한을 확인한다. |
|
|
25
|
+
| SEC-04 | 계정 정지·승인 철회·탈퇴·소속 변경 후 기존 세션·토큰·캐시에서도 권한 회수가 반영된다. |
|
|
26
|
+
| SEC-05 | 코드의 보안 정책·migration·ACL·환경 설정이 대상 배포 환경의 실제 상태와 일치한다. |
|
|
27
|
+
| SEC-06 | 로그인·관리자 인증·세션 만료·로그아웃·비밀번호 재설정·계정 복구가 계정 탈취를 막도록 동작한다. |
|
|
28
|
+
| SEC-07 | 외부 입력·파일 업로드·출력의 형식·크기·경로를 검증하고 SQL 주입·XSS·SSRF·CSRF 등 해당 공격을 방어한다. |
|
|
29
|
+
| SEC-08 | API 키·토큰·개인키·개인정보가 코드·브라우저 번들·로그·점검 기록에 불필요하게 노출되지 않는다. |
|
|
30
|
+
| SEC-09 | 의존성·CI/CD 권한·배포 자격증명·릴리스 산출물의 출처와 무결성을 확인해 공급망 위험을 통제한다. |
|
|
31
|
+
|
|
32
|
+
SEC-01 applicability assessment and SEC-08 note disclosure review are required for every run. Other areas can be inapplicable only with a concrete project and stage reason. A static site may still have deployment credentials, third-party scripts, or dependencies. Lack of access or a missing test is `미검증`, never `해당 없음`.
|
|
33
|
+
|
|
34
|
+
Conditional subchecks belong under the relevant area: atomic multi-record writes and incomplete reads under SEC-01/03; retry/idempotency and concurrent resource changes under SEC-03; stable IDs and organization ownership under SEC-03/04; export permissions and CSV output under SEC-03/07; audit history access/retention under SEC-08. Record applicable subchecks separately so one passing subcheck cannot hide another unchecked subcheck. Static-analysis findings require reproduction or other adequate evidence before dismissal. N+1 and index tuning remain performance concerns unless there is a concrete availability, data-boundary, or integrity risk.
|
|
35
|
+
|
|
36
|
+
## Five statuses and deterministic gate
|
|
37
|
+
|
|
38
|
+
| Status | Meaning | Required-at-stage gate |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| 점검 완료 - 이상 없음 | All applicable required subchecks passed with matching evidence. | Allow |
|
|
41
|
+
| 점검 완료 - 보완 필요 | The check ran and identified an unresolved issue; include severity, owner, follow-up and mitigation. | Block |
|
|
42
|
+
| 점검 실패 | A security requirement failed or a defect was reproduced. | Block |
|
|
43
|
+
| 미검증 | Required evidence is absent, incomplete, stale, or unavailable. | Block |
|
|
44
|
+
| 해당 없음 | No applicable requirement exists for this project and stage; include the reason. | Allow |
|
|
45
|
+
|
|
46
|
+
Overall result is `PASS` only when every required area/subcheck is valid and is either evidenced `이상 없음` or justified `해당 없음`. Otherwise it is `BLOCKED`; show blocking IDs and reasons. Missing or duplicate IDs, missing explanations, invalid statuses, missing binding fields, unfilled template values, or a note-write failure also block the stage. Severity prioritizes remediation and does not silently turn an unresolved finding into a pass. This v1 contract has no automatic waiver path.
|
|
47
|
+
|
|
48
|
+
Some evidence is only available at a later stage. Record `required_at` per area/subcheck and keep the unresolved state visible as `미검증`; for example, live SEC-05 environment inspection may be required at `deploy`. This does not block a commit if the commit-stage policy explicitly defers that live check, but it blocks deployment until verified. Version the stage policy and record it with the run. Do not downgrade a required-at-current-stage check to a later stage merely to obtain `PASS`.
|
|
49
|
+
|
|
50
|
+
## Yam note format
|
|
51
|
+
|
|
52
|
+
In the source repository or package root, use `templates/security-check-note.md`. Installed skills copy references but not templates; use the self-contained starter below when that file is unavailable. `Security check` is an independent block, not a replacement for Study Note or Next step. In changed-artifact reports keep Next step immediately after Study Note; place Security check before that pair. The existing Study Note JSON schema is unchanged.
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
-- Security check --
|
|
56
|
+
|
|
57
|
+
Contract: security-check-v1
|
|
58
|
+
Run: <unique run ID>; Time: <ISO timestamp with timezone>
|
|
59
|
+
Stage: <commit | push | deploy>; Policy: <stage-policy version>
|
|
60
|
+
Environment: <target environment or explicit local/not-applicable reason>
|
|
61
|
+
Binding: <commit: staged tree and base revision | push: outgoing commit IDs and destination | deploy: artifact digest, source commit, environment and configuration revision>
|
|
62
|
+
Scope: <changed paths and connected security boundaries>
|
|
63
|
+
Checker: <tool/version or named reviewer>; Evidence: <sanitized evidence references>
|
|
64
|
+
|
|
65
|
+
1. SEC-01 : "사용자·관리자·외부 서비스·DB·파일 사이의 신뢰 경계와 보호할 데이터·작업을 식별한다." : 미검증
|
|
66
|
+
- Required at: <stages>; Target/evidence/result/time: <actual check details or missing evidence>
|
|
67
|
+
2. SEC-02 : "명시적으로 허용한 접근만 열고, 사용자·서비스 계정에 필요한 최소 권한만 부여한다." : 미검증
|
|
68
|
+
- Required at: <stages>; Target/evidence/result/time: <actual check details or missing evidence>
|
|
69
|
+
3. SEC-03 : "화면을 우회한 API·서버 함수·DB·파일·백그라운드 작업에서도 사용자·조직·대상별 권한을 확인한다." : 미검증
|
|
70
|
+
- Required at: <stages>; Target/evidence/result/time: <actual check details or missing evidence>
|
|
71
|
+
4. SEC-04 : "계정 정지·승인 철회·탈퇴·소속 변경 후 기존 세션·토큰·캐시에서도 권한 회수가 반영된다." : 미검증
|
|
72
|
+
- Required at: <stages>; Target/evidence/result/time: <actual check details or missing evidence>
|
|
73
|
+
5. SEC-05 : "코드의 보안 정책·migration·ACL·환경 설정이 대상 배포 환경의 실제 상태와 일치한다." : 미검증
|
|
74
|
+
- Required at: <stages>; Target/evidence/result/time: <actual check details or missing evidence>
|
|
75
|
+
6. SEC-06 : "로그인·관리자 인증·세션 만료·로그아웃·비밀번호 재설정·계정 복구가 계정 탈취를 막도록 동작한다." : 미검증
|
|
76
|
+
- Required at: <stages>; Target/evidence/result/time: <actual check details or missing evidence>
|
|
77
|
+
7. SEC-07 : "외부 입력·파일 업로드·출력의 형식·크기·경로를 검증하고 SQL 주입·XSS·SSRF·CSRF 등 해당 공격을 방어한다." : 미검증
|
|
78
|
+
- Required at: <stages>; Target/evidence/result/time: <actual check details or missing evidence>
|
|
79
|
+
8. SEC-08 : "API 키·토큰·개인키·개인정보가 코드·브라우저 번들·로그·점검 기록에 불필요하게 노출되지 않는다." : 미검증
|
|
80
|
+
- Required at: <stages>; Target/evidence/result/time: <actual check details or missing evidence>
|
|
81
|
+
9. SEC-09 : "의존성·CI/CD 권한·배포 자격증명·릴리스 산출물의 출처와 무결성을 확인해 공급망 위험을 통제한다." : 미검증
|
|
82
|
+
- Required at: <stages>; Target/evidence/result/time: <actual check details or missing evidence>
|
|
83
|
+
|
|
84
|
+
Result: BLOCKED
|
|
85
|
+
Blocking: <required-at-current-stage unresolved IDs/subchecks or invalid record fields>
|
|
86
|
+
Deferred: <later-stage missing evidence, required stage, owner and action; none if absent>
|
|
87
|
+
Retry of: <prior run ID or none>
|
|
88
|
+
-- End Security check --
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
This full starter contains all nine IDs and defaults to `미검증`/`BLOCKED`; copying it cannot create successful evidence. For `보완 필요` or `점검 실패`, record the finding, severity, owner, follow-up action/date and mitigation. For `해당 없음`, record the concrete project/stage reason.
|
|
92
|
+
|
|
93
|
+
Each detail records required stages, actual checked target, command/test/review reference, result and time, plus any skipped subcheck. Human review is acceptable for design checks when the reviewer and inspected scope are explicit. A status supplied by an agent is not independent runtime evidence. Do not call the whole system secure from this checklist alone.
|
|
94
|
+
|
|
95
|
+
## Persistence and enforcement boundaries
|
|
96
|
+
|
|
97
|
+
- Append one complete block per run to the project's existing Yam note. If none exists, the default is `.yam/security/notes.md`; keep it out of published package contents. Do not overwrite user notes or silently initialize Git hooks.
|
|
98
|
+
- Failed runs also leave a record. Preserve earlier records and connect retries by run ID; never rewrite a failure into a pass. Append-only behavior is a design requirement, not tamper-proof storage against the same OS user.
|
|
99
|
+
- Store only redacted findings and references. Never include secret values, credential-bearing URLs, raw environment dumps, personal data, or malicious payload contents in the note. If safe persistence fails, block and report a sanitized error.
|
|
100
|
+
- The validator rejects incomplete coverage and mismatched bindings; checks remeasure staged content, outgoing refs, or deployment files before persistence. Note writes are serialized with a bounded lock. Unsafe paths, links, duplicate run IDs and write failures block. A run JSON may remain if its note append fails: this is not a successful gate.
|
|
101
|
+
- Focused hook/CI tests cover normal pass, improvement needed, failure, missing evidence, justified non-applicability, staged/unstaged mismatch, changed outgoing revisions, changed deployment artifact/configuration, missing note writes, bypassed local hooks, and note secret redaction.
|
|
102
|
+
- `security init` and `security hooks enable` explicitly configure a project. No global hooks are installed by package installation. Remote branch protection and deployment authorization are outside the local CLI; until the required CI job and deployment dependency are configured, remote enforcement is incomplete.
|
|
103
|
+
|
|
104
|
+
## Runtime commands
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
yam security init
|
|
108
|
+
yam security status
|
|
109
|
+
yam template security
|
|
110
|
+
yam security check --stage commit --json
|
|
111
|
+
yam security hooks enable
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Init preserves existing files, creates a private draft at `.yam/security/check.json`, a policy at `.yam/security/policy.json`, and pins the exact policy file digest in Git-local `yam-security.json`. Every draft area is unverified. Replace draft values with real stage-bound review/test evidence before enabling workflows. Init does not grant PASS. Report fields follow `SecurityCheckInput` in `src/lib/security-check.ts`; `createSecurityStarter` and `collectSecurityBinding` provide exact report construction for each stage. Evidence uses repository-relative regular files, `sha256:<64 hex>` of their bytes, checker, checked_at, and `result: "passed"` for passing checks. IDs, canonical explanations and stage requirements are mandatory.
|
|
115
|
+
|
|
116
|
+
Policy uses `version` and a `required_at` map for all nine IDs. SEC-01/08 must apply at every stage; other areas may defer only through the reviewed policy. CI maps to push requirements but has a distinct source/base/tree binding, so a local push report cannot stand in for a CI report. The exact policy file bytes are pinned, including whitespace. Reapprove a changed policy externally; do not take a policy pin from the same untrusted change that it is intended to verify.
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
# pre-push hook supplies refs on stdin and the actual remote destination.
|
|
120
|
+
yam security check --stage push --remote origin --destination <actual-destination> --json
|
|
121
|
+
# CI independently validates its report; policy pin comes from trusted runner settings.
|
|
122
|
+
yam security check --stage ci --source <exact-commit> --base <exact-base> --policy security/policy.json --policy-digest <trusted-sha256> --evidence security/check.json --json
|
|
123
|
+
# Deployment measures actual artifact/configuration file bytes as well as matching the report.
|
|
124
|
+
yam security check --stage deploy --source <exact-commit> --artifact <file> --artifact-digest <sha256> --configuration <file> --configuration-digest <sha256> --environment production --policy <file> --policy-digest <trusted-sha256> --evidence <report> --json
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`--note <repository-local-file>` can append to an existing Yam note. The default private note is `.yam/security/notes.md`; JSON runs go under `.yam/security/runs/`. Hooks refuse existing hooks and custom `core.hooksPath` rather than overwrite them. POSIX hook setup is supported; Windows and custom hooks need manual integration. Ref deletion pushes fail closed pending a separately reviewed workflow. CI initial pushes without a valid base also fail closed; use a reviewed exact base instead of fabricating a prior revision.
|
|
128
|
+
|
|
129
|
+
`yam template security-ci` prints `templates/security-gate.yml`. It is a project setup template, not an active workflow in this repository: provision the trusted yam runner, approved policy pin, real test/review evidence, artifact retention and required branch check before enabling it. Put the same deployment gate immediately before the deployment operation, make later steps depend on success, and restrict bypass routes. Running a local command cannot change hosting-provider branch rules. Do not run untrusted project code with production credentials.
|
|
130
|
+
|
|
131
|
+
Conditional subchecks must be represented in the referenced review/test evidence for their parent area. The validator does not discover omitted application requirements or execute project scanners. Supply independently generated evidence or an identified reviewer; operator-authored status and hash alone can be fabricated. Deploy configuration file hashes prove file identity, not cloud state: include actual environment inspection evidence for SEC-05. Secret-pattern rejection is best effort and not a comprehensive privacy classifier; record only sanitized references and short findings, never raw personal data or credential values. Same-user concurrent filesystem replacement cannot be fully prevented with Node path checks; this is not an adversarial same-user sandbox.
|