200 lines
7.1 KiB
JavaScript
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);
|
|
}
|
|
});
|
|
});
|