Files
2026-07-13 12:55:23 +08:00

200 lines
7.1 KiB
JavaScript

const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '..');
const CLI = path.join(ROOT, 'src', 'cli.js');
const OUTPUT_DIR = path.join(ROOT, 'output', 'security-test');
beforeAll(() => {
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
});
afterAll(() => {
if (fs.existsSync(OUTPUT_DIR)) {
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
}
});
describe('sketch-cli security', () => {
test('allows tokens in folders starting with dots (..brand)', () => {
const specDir = path.join(OUTPUT_DIR, 'spec');
const brandDir = path.join(specDir, '..brand');
fs.mkdirSync(brandDir, { recursive: true });
const tokensPath = path.join(brandDir, 'tokens.json');
const tokens = { colors: { primary: '#ff0000' } };
fs.writeFileSync(tokensPath, JSON.stringify(tokens));
const inputPath = path.join(specDir, 'design.json');
const design = {
pages: [{ do_objectID: 'page1', layers: [], artboards: [] }],
tokens: './..brand/tokens.json'
};
fs.writeFileSync(inputPath, JSON.stringify(design));
const outputPath = path.join(OUTPUT_DIR, 'out.sketch');
const result = execSync(
`node "${CLI}" build --input "${inputPath}" --output "${outputPath}" 2>&1`,
{ encoding: 'utf-8', cwd: ROOT }
);
expect(result).toContain('Done!');
expect(result).not.toContain('Unsafe spec tokens path ignored');
});
test('blocks tokens outside the spec directory', () => {
const specDir = path.join(OUTPUT_DIR, 'spec-unsafe');
fs.mkdirSync(specDir, { recursive: true });
const unsafeTokensPath = path.join(OUTPUT_DIR, 'unsafe-tokens.json');
const tokens = { colors: { primary: '#00ff00' } };
fs.writeFileSync(unsafeTokensPath, JSON.stringify(tokens));
const inputPath = path.join(specDir, 'design.json');
const design = {
pages: [{ do_objectID: 'page1', layers: [], artboards: [] }],
tokens: '../../unsafe-tokens.json'
};
fs.writeFileSync(inputPath, JSON.stringify(design));
const outputPath = path.join(OUTPUT_DIR, 'out-unsafe.sketch');
const result = execSync(
`node "${CLI}" build --input "${inputPath}" --output "${outputPath}" 2>&1`,
{ encoding: 'utf-8', cwd: ROOT }
);
expect(result).toContain('Unsafe spec tokens path ignored');
});
test('blocks symlinks pointing outside the spec directory', () => {
const specDir = path.join(OUTPUT_DIR, 'spec-symlink');
fs.mkdirSync(specDir, { recursive: true });
const secretPath = path.join(OUTPUT_DIR, 'secret.json');
fs.writeFileSync(secretPath, JSON.stringify({ secret: 'ssh-key' }));
const symlinkPath = path.join(specDir, 'malicious-link.json');
try {
fs.symlinkSync(secretPath, symlinkPath);
} catch (e) {
console.warn('Skipping symlink test: symlinks not supported/permitted');
return;
}
const inputPath = path.join(specDir, 'design.json');
const design = {
pages: [{ do_objectID: 'page1', layers: [], artboards: [] }],
tokens: './malicious-link.json'
};
fs.writeFileSync(inputPath, JSON.stringify(design));
const outputPath = path.join(OUTPUT_DIR, 'out-symlink.sketch');
const result = execSync(
`node "${CLI}" build --input "${inputPath}" --output "${outputPath}" 2>&1`,
{ encoding: 'utf-8', cwd: ROOT }
);
expect(result).toContain('Unsafe spec tokens path ignored');
});
test('allows symlinks pointing inside the spec directory', () => {
const specDir = path.join(OUTPUT_DIR, 'spec-symlink-safe');
fs.mkdirSync(specDir, { recursive: true });
const actualTokensPath = path.join(specDir, 'actual-tokens.json');
fs.writeFileSync(actualTokensPath, JSON.stringify({ colors: { primary: '#0000ff' } }));
const symlinkPath = path.join(specDir, 'safe-link.json');
try {
fs.symlinkSync(actualTokensPath, symlinkPath);
} catch (e) {
console.warn('Skipping safe symlink test: symlinks not supported/permitted');
return;
}
const inputPath = path.join(specDir, 'design.json');
const design = {
pages: [{ do_objectID: 'page1', layers: [], artboards: [] }],
tokens: './safe-link.json'
};
fs.writeFileSync(inputPath, JSON.stringify(design));
const outputPath = path.join(OUTPUT_DIR, 'out-safe-symlink.sketch');
const result = execSync(
`node "${CLI}" build --input "${inputPath}" --output "${outputPath}" 2>&1`,
{ encoding: 'utf-8', cwd: ROOT }
);
expect(result).toContain('Done!');
expect(result).not.toContain('Unsafe spec tokens path ignored');
});
test('allows spec tokens from the global project tokens directory', () => {
const specDir = path.join(OUTPUT_DIR, 'spec-global-tokens');
fs.mkdirSync(specDir, { recursive: true });
// We expect it to reach the actual global tokens directory in CLI-Anything/sketch/agent-harness/tokens
// For this test, we can mock the behavior by pointing to the real tokens dir
const tokensDir = path.resolve(ROOT, 'tokens');
const brandTokensPath = path.join(tokensDir, 'brand-test.json');
fs.writeFileSync(brandTokensPath, JSON.stringify({ colors: { primary: '#666666' } }));
const inputPath = path.join(specDir, 'design.json');
const design = {
pages: [{ do_objectID: 'page1', layers: [], artboards: [] }],
tokens: '../../../tokens/brand-test.json' // Correct relative path to reach sketch/agent-harness/tokens
};
fs.writeFileSync(inputPath, JSON.stringify(design));
const outputPath = path.join(OUTPUT_DIR, 'out-global.sketch');
try {
const result = execSync(
`node "${CLI}" build --input "${inputPath}" --output "${outputPath}" 2>&1`,
{ encoding: 'utf-8', cwd: ROOT }
);
expect(result).toContain('Done!');
expect(result).not.toContain('Unsafe spec tokens path ignored');
} finally {
if (fs.existsSync(brandTokensPath)) fs.unlinkSync(brandTokensPath);
}
});
test('allows CLI tokens from the current working directory (CWD)', () => {
// In this test, the ROOT of the harness is the CWD for the command
const cwdTokensPath = path.join(ROOT, 'cwd-tokens.json');
fs.writeFileSync(cwdTokensPath, JSON.stringify({ colors: { primary: '#333333' } }));
const specDir = path.join(OUTPUT_DIR, 'spec-cwd');
fs.mkdirSync(specDir, { recursive: true });
const inputPath = path.join(specDir, 'design.json');
const design = {
pages: [{ do_objectID: 'page1', layers: [], artboards: [] }]
};
fs.writeFileSync(inputPath, JSON.stringify(design));
const outputPath = path.join(OUTPUT_DIR, 'out-cwd.sketch');
try {
const result = execSync(
`node "${CLI}" build --input "${inputPath}" --output "${outputPath}" --tokens cwd-tokens.json 2>&1`,
{ encoding: 'utf-8', cwd: ROOT }
);
expect(result).toContain('Done!');
expect(result).not.toContain('Unsafe tokens path ignored');
} finally {
if (fs.existsSync(cwdTokensPath)) fs.unlinkSync(cwdTokensPath);
}
});
});