@birdapi/velinstyle 1.2.2 → 1.3.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/README.de.md +18 -4
- package/README.md +42 -7
- package/cli/cli-manifest.json +441 -174
- package/cli/docgen/extract-cli.js +2 -1
- package/cli/docs-generate.js +9 -0
- package/cli/experience.js +173 -0
- package/cli/index.js +1294 -417
- package/cli/motion.js +738 -0
- package/cli/production/extract.js +46 -6
- package/cli/production/report.js +104 -14
- package/cli/production/run.js +44 -1
- package/cli/security.js +190 -0
- package/cli/skills.js +53 -10
- package/cli/transparency.js +140 -41
- package/cli/workflow.js +32 -17
- package/components/velin-theme-toggle.js +15 -1
- package/core/a11y/component-contracts.json +391 -274
- package/core/attributes/registry.js +1 -1
- package/core/motion/analyze.js +38 -0
- package/core/motion/blueprint.js +51 -0
- package/core/motion/composer.js +114 -0
- package/core/motion/data/registry.json +352 -0
- package/core/motion/effects.js +17 -1
- package/core/motion/index.js +112 -9
- package/core/motion/optimize.js +44 -0
- package/core/motion/policy.js +77 -0
- package/core/motion/registry.js +131 -0
- package/core/motion/review.js +59 -0
- package/core/motion/scan.js +150 -0
- package/core/motion/timeline.js +68 -0
- package/core/motion/transitions.js +49 -0
- package/core/motion/triggers.js +168 -0
- package/core/security/checks/fs.js +92 -0
- package/core/security/detect.js +35 -0
- package/core/security/engine.js +116 -0
- package/core/security/index.js +18 -0
- package/core/security/registry.js +85 -0
- package/core/security/report/report.js +113 -0
- package/core/security/rules/ci/index.js +146 -0
- package/core/security/rules/consumer/index.js +174 -0
- package/core/security/rules/deps/index.js +182 -0
- package/core/security/rules/index.js +24 -0
- package/core/security/rules/publish/index.js +230 -0
- package/core/security/rules/repo/index.js +143 -0
- package/core/security/rules/secrets/index.js +133 -0
- package/core/security/score/dependency-health.js +53 -0
- package/core/security/score/release-health.js +66 -0
- package/core/security/score/score.js +102 -0
- package/core/security/timeline/timeline.js +76 -0
- package/dist/chunks/attributes-HK7VIOLA.js +1080 -0
- package/dist/chunks/attributes-VRHHVNDO.js +1080 -0
- package/dist/chunks/chunk-MYKUI364.js +116 -0
- package/dist/chunks/chunk-NEVBPD5T.js +116 -0
- package/dist/chunks/runtime-entry.js +1 -1
- package/dist/chunks/velin-theme-toggle-J2NHC7IM.js +304 -0
- package/dist/llms.txt +2 -2
- package/dist/search-index.json +166 -2
- package/dist/velin-agent.json +156 -32
- package/dist/velinstyle-components.iife.js +862 -17
- package/dist/velinstyle-components.js +862 -17
- package/dist/velinstyle-components.min.js +126 -126
- package/dist/velinstyle.css +42 -7
- package/dist/velinstyle.min.css +1 -1
- package/package.json +158 -143
- package/packages/velinstyle-cli-core/package.json +11 -0
- package/packages/velinstyle-cli-core/src/config.js +84 -0
- package/packages/velinstyle-cli-core/src/contract.js +56 -0
- package/packages/velinstyle-cli-core/src/event-bus.js +50 -0
- package/packages/velinstyle-cli-core/src/index.js +18 -0
- package/packages/velinstyle-cli-core/src/lifecycle.js +33 -0
- package/packages/velinstyle-cli-core/src/runner.js +129 -0
- package/packages/velinstyle-cli-registry/data/commands.json +1216 -0
- package/packages/velinstyle-cli-registry/data/layouts.json +33 -0
- package/packages/velinstyle-cli-registry/data/widgets.json +35 -0
- package/packages/velinstyle-cli-registry/package.json +13 -0
- package/packages/velinstyle-cli-registry/src/index.js +88 -0
- package/packages/velinstyle-cli-renderer/package.json +8 -0
- package/packages/velinstyle-cli-renderer/src/index.js +286 -0
- package/packages/velinstyle-cli-theme/package.json +8 -0
- package/packages/velinstyle-cli-theme/src/index.js +84 -0
- package/packages/velinstyle-cli-ui/package.json +8 -0
- package/packages/velinstyle-cli-ui/src/index.js +34 -0
- package/packages/velinstyle-motion/README.md +81 -0
- package/packages/velinstyle-motion/package.json +37 -0
- package/packages/velinstyle-motion/src/ai/mini-prompt.js +119 -0
- package/packages/velinstyle-motion/src/ai/provider.js +59 -0
- package/packages/velinstyle-motion/src/ai/providers/pixverse-setup.js +402 -0
- package/packages/velinstyle-motion/src/ai/providers/pixverse.js +245 -0
- package/packages/velinstyle-motion/src/config.js +257 -0
- package/packages/velinstyle-motion/src/export/html-clip.js +141 -0
- package/packages/velinstyle-motion/src/index.js +304 -0
- package/packages/velinstyle-motion/src/jobs/cache.js +30 -0
- package/packages/velinstyle-motion/src/jobs/queue.js +69 -0
- package/packages/velinstyle-motion/src/jobs/usage.js +53 -0
- package/packages/velinstyle-motion/src/local/effects.js +139 -0
- package/packages/velinstyle-motion/src/local/engine.js +96 -0
- package/packages/velinstyle-motion/src/providers-catalog.js +44 -0
- package/packages/velinstyle-motion/src/registry/presets.js +25 -0
- package/packages/velinstyle-motion/src/registry/presets.json +85 -0
- package/packages/velinstyle-motion/src/ux/cost-gate.js +37 -0
- package/packages/velinstyle-motion/src/ux/pixverse-gate.js +153 -0
- package/packages/velinstyle-motion/src/ux/wizard.js +171 -0
- package/packages/velinstyle-motion/src/validate-image.js +91 -0
- package/packages/velinstyle-skills/catalog.json +1 -1
- package/packages/velinstyle-skills/registry.json +1 -142
- package/packages/velinstyle-skills/skills/velin-motion-reduced-safe/SKILL.md +1 -1
- package/packages/velinstyle-skills/skills/velin-motion-reveal-feedback/SKILL.md +1 -1
- package/src/tokens/motion.css +7 -0
- package/src/utilities/scroll-animation.css +66 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security report builders (JSON / Markdown / Experience payload).
|
|
3
|
+
*/
|
|
4
|
+
import { gateLabel } from '../score/score.js';
|
|
5
|
+
import { timelineRows } from '../timeline/timeline.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {object} result — engine result
|
|
9
|
+
*/
|
|
10
|
+
export function toExperiencePayload(result) {
|
|
11
|
+
const { securityScore, scores, overall, dependencyHealth, releaseHealth, timeline, mode, findings } = result;
|
|
12
|
+
const items = [
|
|
13
|
+
{ label: 'Security Score', value: String(securityScore) },
|
|
14
|
+
{ label: 'Overall', value: overall },
|
|
15
|
+
{ label: 'Mode', value: mode },
|
|
16
|
+
];
|
|
17
|
+
if (dependencyHealth) {
|
|
18
|
+
items.push({ label: 'Dependency Health', value: `${dependencyHealth.score} %` });
|
|
19
|
+
}
|
|
20
|
+
if (releaseHealth) {
|
|
21
|
+
items.push({ label: 'Release Health', value: releaseHealth.status });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const tableRows = Object.entries(scores || {}).map(([k, v]) => [
|
|
25
|
+
v.label || k,
|
|
26
|
+
gateLabel(v.gate),
|
|
27
|
+
v.na ? 'n/a' : String(v.score),
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const notifications = [];
|
|
31
|
+
if (overall === 'BLOCKED') {
|
|
32
|
+
notifications.push({ tone: 'danger', message: 'Security gate BLOCKED — fix errors before publish' });
|
|
33
|
+
} else if (overall === 'READY_WITH_WARNINGS') {
|
|
34
|
+
notifications.push({ tone: 'warning', message: 'Ready with warnings — review WARNING findings' });
|
|
35
|
+
} else {
|
|
36
|
+
notifications.push({ tone: 'success', message: 'Security Overall READY' });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const tl = timelineRows(timeline || { entries: {} });
|
|
40
|
+
if (tl.length) {
|
|
41
|
+
notifications.push({
|
|
42
|
+
tone: 'info',
|
|
43
|
+
message: `Timeline: ${tl.map((r) => `${r.title} ${r.date}`).join(' · ')}`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
message: `Security Score ${securityScore} / 100 · ${overall}`,
|
|
49
|
+
items,
|
|
50
|
+
table: {
|
|
51
|
+
columns: ['Area', 'Gate', 'Score'],
|
|
52
|
+
rows: tableRows,
|
|
53
|
+
},
|
|
54
|
+
score: { value: securityScore, label: 'Security' },
|
|
55
|
+
findings: (findings || []).slice(0, 40).map((f) => ({
|
|
56
|
+
severity: f.severity,
|
|
57
|
+
message: f.message,
|
|
58
|
+
path: f.path,
|
|
59
|
+
rule: f.ruleId,
|
|
60
|
+
})),
|
|
61
|
+
notifications,
|
|
62
|
+
meta: {
|
|
63
|
+
dependencyHealth,
|
|
64
|
+
releaseHealth,
|
|
65
|
+
timeline: tl,
|
|
66
|
+
mode,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {object} result
|
|
73
|
+
*/
|
|
74
|
+
export function toMarkdownReport(result) {
|
|
75
|
+
const lines = [
|
|
76
|
+
'# Velin Security Report',
|
|
77
|
+
'',
|
|
78
|
+
`**Score:** ${result.securityScore} / 100`,
|
|
79
|
+
`**Overall:** ${result.overall}`,
|
|
80
|
+
`**Mode:** ${result.mode}`,
|
|
81
|
+
'',
|
|
82
|
+
'## Categories',
|
|
83
|
+
'',
|
|
84
|
+
];
|
|
85
|
+
for (const [k, v] of Object.entries(result.scores || {})) {
|
|
86
|
+
const scorePart = v.na ? 'n/a' : String(v.score);
|
|
87
|
+
const note = v.note ? ` — ${v.note}` : '';
|
|
88
|
+
lines.push(`- **${v.label || k}:** ${gateLabel(v.gate)} (${scorePart})${note}`);
|
|
89
|
+
}
|
|
90
|
+
if (result.dependencyHealth) {
|
|
91
|
+
lines.push('', '## Dependency Health', '');
|
|
92
|
+
const dh = result.dependencyHealth;
|
|
93
|
+
lines.push(
|
|
94
|
+
`Score ${dh.score}% · deprecated ${dh.deprecated} · no-lockfile ${dh.noLockfile ?? 0} · outdated (heuristic) ${dh.outdated} · critical ${dh.critical}`,
|
|
95
|
+
);
|
|
96
|
+
if (dh.note) lines.push(`_${dh.note}_`);
|
|
97
|
+
}
|
|
98
|
+
if (result.releaseHealth) {
|
|
99
|
+
lines.push('', '## Release Health', '');
|
|
100
|
+
lines.push(`**${result.releaseHealth.status}** (score ${result.releaseHealth.score})`);
|
|
101
|
+
}
|
|
102
|
+
const tl = timelineRows(result.timeline || { entries: {} });
|
|
103
|
+
if (tl.length) {
|
|
104
|
+
lines.push('', '## Security Timeline', '');
|
|
105
|
+
for (const r of tl) lines.push(`- ${r.title}: ${r.date}`);
|
|
106
|
+
}
|
|
107
|
+
lines.push('', '## Findings', '');
|
|
108
|
+
for (const f of result.findings || []) {
|
|
109
|
+
lines.push(`- **${f.severity}** \`${f.ruleId}\`: ${f.message}${f.path ? ` (${f.path})` : ''}`);
|
|
110
|
+
}
|
|
111
|
+
lines.push('');
|
|
112
|
+
return lines.join('\n');
|
|
113
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { registerSecurityRule } from '../../registry.js';
|
|
2
|
+
import { fileExists, readText, walkFiles, rel, workflowHints, isShaPinned } from '../../checks/fs.js';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
export function registerCiRules() {
|
|
6
|
+
registerSecurityRule({
|
|
7
|
+
id: 'workflow.present',
|
|
8
|
+
category: 'ci',
|
|
9
|
+
severity: 'info',
|
|
10
|
+
modes: ['repo', 'ci', 'audit'],
|
|
11
|
+
title: 'Workflows directory',
|
|
12
|
+
run(ctx) {
|
|
13
|
+
if (fileExists(join(ctx.root, '.github', 'workflows'))) return [];
|
|
14
|
+
return [{
|
|
15
|
+
ruleId: 'workflow.present',
|
|
16
|
+
category: 'ci',
|
|
17
|
+
severity: 'info',
|
|
18
|
+
message: 'No .github/workflows directory',
|
|
19
|
+
path: '.github/workflows',
|
|
20
|
+
}];
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
registerSecurityRule({
|
|
25
|
+
id: 'workflow.sha-pinning',
|
|
26
|
+
category: 'ci',
|
|
27
|
+
severity: 'warning',
|
|
28
|
+
modes: ['repo', 'ci', 'audit', 'publish'],
|
|
29
|
+
title: 'SHA pinning',
|
|
30
|
+
description: 'GitHub Actions should pin actions by commit SHA.',
|
|
31
|
+
run(ctx) {
|
|
32
|
+
const dir = join(ctx.root, '.github', 'workflows');
|
|
33
|
+
if (!fileExists(dir)) return [];
|
|
34
|
+
const files = walkFiles(dir, (n) => /\.ya?ml$/i.test(n), 40);
|
|
35
|
+
const findings = [];
|
|
36
|
+
for (const abs of files) {
|
|
37
|
+
const hints = workflowHints(readText(abs) || '');
|
|
38
|
+
for (const u of hints.uses) {
|
|
39
|
+
if (!isShaPinned(u) && /@(v?\d|main|master|latest)/i.test(u)) {
|
|
40
|
+
findings.push({
|
|
41
|
+
ruleId: 'workflow.sha-pinning',
|
|
42
|
+
category: 'ci',
|
|
43
|
+
severity: 'warning',
|
|
44
|
+
message: `Action not SHA-pinned: ${u}`,
|
|
45
|
+
path: rel(ctx.root, abs),
|
|
46
|
+
fix: 'Pin uses: owner/action@<40-char-sha>',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return findings;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
registerSecurityRule({
|
|
56
|
+
id: 'workflow.pull-request-target',
|
|
57
|
+
category: 'ci',
|
|
58
|
+
severity: 'error',
|
|
59
|
+
modes: ['repo', 'ci', 'audit'],
|
|
60
|
+
title: 'pull_request_target',
|
|
61
|
+
run(ctx) {
|
|
62
|
+
const dir = join(ctx.root, '.github', 'workflows');
|
|
63
|
+
if (!fileExists(dir)) return [];
|
|
64
|
+
const files = walkFiles(dir, (n) => /\.ya?ml$/i.test(n), 40);
|
|
65
|
+
const findings = [];
|
|
66
|
+
for (const abs of files) {
|
|
67
|
+
const hints = workflowHints(readText(abs) || '');
|
|
68
|
+
if (hints.hasPullRequestTarget) {
|
|
69
|
+
findings.push({
|
|
70
|
+
ruleId: 'workflow.pull-request-target',
|
|
71
|
+
category: 'ci',
|
|
72
|
+
severity: 'error',
|
|
73
|
+
message: 'Workflow uses pull_request_target — high risk if checkout PR code',
|
|
74
|
+
path: rel(ctx.root, abs),
|
|
75
|
+
fix: 'Avoid untrusted code execution on pull_request_target',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return findings;
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
registerSecurityRule({
|
|
84
|
+
id: 'workflow.permissions',
|
|
85
|
+
category: 'ci',
|
|
86
|
+
severity: 'warning',
|
|
87
|
+
modes: ['repo', 'ci', 'audit'],
|
|
88
|
+
title: 'Explicit permissions',
|
|
89
|
+
run(ctx) {
|
|
90
|
+
const dir = join(ctx.root, '.github', 'workflows');
|
|
91
|
+
if (!fileExists(dir)) return [];
|
|
92
|
+
const files = walkFiles(dir, (n) => /\.ya?ml$/i.test(n), 40);
|
|
93
|
+
const findings = [];
|
|
94
|
+
for (const abs of files) {
|
|
95
|
+
const hints = workflowHints(readText(abs) || '');
|
|
96
|
+
if (!hints.hasPermissions) {
|
|
97
|
+
findings.push({
|
|
98
|
+
ruleId: 'workflow.permissions',
|
|
99
|
+
category: 'ci',
|
|
100
|
+
severity: 'warning',
|
|
101
|
+
message: 'Workflow missing top-level permissions: block',
|
|
102
|
+
path: rel(ctx.root, abs),
|
|
103
|
+
fix: 'Set least-privilege permissions',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return findings;
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
registerSecurityRule({
|
|
112
|
+
id: 'workflow.publish-token',
|
|
113
|
+
category: 'ci',
|
|
114
|
+
severity: 'info',
|
|
115
|
+
modes: ['repo', 'ci', 'audit', 'publish'],
|
|
116
|
+
title: 'Publish token / EOTP',
|
|
117
|
+
run(ctx) {
|
|
118
|
+
const pub = join(ctx.root, '.github', 'workflows', 'publish-npm.yml');
|
|
119
|
+
if (!fileExists(pub)) return [];
|
|
120
|
+
const text = readText(pub) || '';
|
|
121
|
+
const findings = [];
|
|
122
|
+
if (/NPM_TOKEN|NODE_AUTH_TOKEN/.test(text)) {
|
|
123
|
+
findings.push({
|
|
124
|
+
ruleId: 'workflow.publish-token',
|
|
125
|
+
category: 'ci',
|
|
126
|
+
severity: 'info',
|
|
127
|
+
message: 'Publish workflow uses NPM_TOKEN — use npm Automation token (no OTP/EOTP)',
|
|
128
|
+
path: '.github/workflows/publish-npm.yml',
|
|
129
|
+
fix: 'Granular Automation token with publish access; avoid classic 2FA tokens in CI',
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const hints = workflowHints(text);
|
|
133
|
+
if (!hints.hasIdToken) {
|
|
134
|
+
findings.push({
|
|
135
|
+
ruleId: 'workflow.publish-provenance',
|
|
136
|
+
category: 'ci',
|
|
137
|
+
severity: 'warning',
|
|
138
|
+
message: 'Publish workflow may lack id-token: write for npm provenance',
|
|
139
|
+
path: '.github/workflows/publish-npm.yml',
|
|
140
|
+
fix: 'Add permissions.id-token: write for provenance',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return findings;
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { registerSecurityRule } from '../../registry.js';
|
|
2
|
+
import { fileExists, readJson, readText, walkFiles, rel } from '../../checks/fs.js';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
export function registerConsumerRules() {
|
|
6
|
+
registerSecurityRule({
|
|
7
|
+
id: 'consumer.velinstyle-installed',
|
|
8
|
+
category: 'consumer',
|
|
9
|
+
severity: 'warning',
|
|
10
|
+
modes: ['consumer', 'audit', 'scan'],
|
|
11
|
+
title: 'VelinStyle installed',
|
|
12
|
+
run(ctx) {
|
|
13
|
+
const pkg = readJson(join(ctx.root, 'package.json')) || {};
|
|
14
|
+
if (pkg.name === '@birdapi/velinstyle') return [];
|
|
15
|
+
const dep = pkg.dependencies?.['@birdapi/velinstyle'] || pkg.devDependencies?.['@birdapi/velinstyle'];
|
|
16
|
+
const vendor =
|
|
17
|
+
fileExists(join(ctx.root, 'vendor', 'velinstyle.min.css'))
|
|
18
|
+
|| fileExists(join(ctx.root, 'vendor', 'velinstyle', 'velinstyle.min.css'));
|
|
19
|
+
if (dep || vendor) return [];
|
|
20
|
+
return [{
|
|
21
|
+
ruleId: 'consumer.velinstyle-installed',
|
|
22
|
+
category: 'consumer',
|
|
23
|
+
severity: 'warning',
|
|
24
|
+
message: '@birdapi/velinstyle not found in package.json or vendor/',
|
|
25
|
+
fix: 'npm i @birdapi/velinstyle or copy dist to vendor/',
|
|
26
|
+
}];
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
registerSecurityRule({
|
|
31
|
+
id: 'consumer.version-floor',
|
|
32
|
+
category: 'consumer',
|
|
33
|
+
severity: 'warning',
|
|
34
|
+
modes: ['consumer', 'audit'],
|
|
35
|
+
title: 'Minimum recommended version',
|
|
36
|
+
run(ctx) {
|
|
37
|
+
const pkg = readJson(join(ctx.root, 'package.json')) || {};
|
|
38
|
+
if (pkg.name === '@birdapi/velinstyle') return [];
|
|
39
|
+
const raw = pkg.dependencies?.['@birdapi/velinstyle'] || pkg.devDependencies?.['@birdapi/velinstyle'];
|
|
40
|
+
if (!raw) return [];
|
|
41
|
+
const m = String(raw).match(/(\d+)\.(\d+)\.(\d+)/);
|
|
42
|
+
if (!m) return [];
|
|
43
|
+
const [maj, min, pat] = m.slice(1).map(Number);
|
|
44
|
+
const tooOld = maj < 1 || (maj === 1 && min < 2) || (maj === 1 && min === 2 && pat < 2);
|
|
45
|
+
if (!tooOld) return [];
|
|
46
|
+
return [{
|
|
47
|
+
ruleId: 'consumer.version-floor',
|
|
48
|
+
category: 'consumer',
|
|
49
|
+
severity: 'warning',
|
|
50
|
+
message: `VelinStyle ${raw} is below recommended 1.2.2+`,
|
|
51
|
+
code: 'outdated',
|
|
52
|
+
fix: 'Upgrade to @birdapi/velinstyle@^1.2.2',
|
|
53
|
+
}];
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
registerSecurityRule({
|
|
58
|
+
id: 'consumer.noopener',
|
|
59
|
+
category: 'consumer',
|
|
60
|
+
severity: 'warning',
|
|
61
|
+
modes: ['consumer', 'scan', 'audit'],
|
|
62
|
+
title: 'target=_blank noopener',
|
|
63
|
+
run(ctx) {
|
|
64
|
+
const files = walkFiles(ctx.root, (n) => /\.html?$/i.test(n), 80);
|
|
65
|
+
const findings = [];
|
|
66
|
+
for (const abs of files) {
|
|
67
|
+
const t = readText(abs) || '';
|
|
68
|
+
const re = /target=["']_blank["'](?![^>]*rel=["'][^"']*noopener)/gi;
|
|
69
|
+
if (re.test(t)) {
|
|
70
|
+
findings.push({
|
|
71
|
+
ruleId: 'consumer.noopener',
|
|
72
|
+
category: 'consumer',
|
|
73
|
+
severity: 'warning',
|
|
74
|
+
message: 'target="_blank" without rel="noopener noreferrer"',
|
|
75
|
+
path: rel(ctx.root, abs),
|
|
76
|
+
fix: 'Add rel="noopener noreferrer"',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return findings.slice(0, 20);
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
registerSecurityRule({
|
|
85
|
+
id: 'consumer.inline-script',
|
|
86
|
+
category: 'consumer',
|
|
87
|
+
severity: 'warning',
|
|
88
|
+
modes: ['consumer', 'scan', 'audit'],
|
|
89
|
+
title: 'Inline scripts',
|
|
90
|
+
run(ctx) {
|
|
91
|
+
const files = walkFiles(ctx.root, (n) => /\.html?$/i.test(n), 80);
|
|
92
|
+
const findings = [];
|
|
93
|
+
for (const abs of files) {
|
|
94
|
+
const t = readText(abs) || '';
|
|
95
|
+
if (/<script(?![^>]*\bsrc=)[^>]*>\s*[^<\s]/i.test(t)) {
|
|
96
|
+
findings.push({
|
|
97
|
+
ruleId: 'consumer.inline-script',
|
|
98
|
+
category: 'consumer',
|
|
99
|
+
severity: 'warning',
|
|
100
|
+
message: 'Inline <script> increases XSS surface',
|
|
101
|
+
path: rel(ctx.root, abs),
|
|
102
|
+
fix: 'Move to external module',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return findings.slice(0, 15);
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
registerSecurityRule({
|
|
111
|
+
id: 'consumer.csp-meta',
|
|
112
|
+
category: 'consumer',
|
|
113
|
+
severity: 'info',
|
|
114
|
+
modes: ['consumer', 'scan', 'audit'],
|
|
115
|
+
title: 'CSP meta',
|
|
116
|
+
run(ctx) {
|
|
117
|
+
const files = walkFiles(ctx.root, (n) => /\.html?$/i.test(n), 40);
|
|
118
|
+
if (!files.length) return [];
|
|
119
|
+
const any = files.some((abs) => /Content-Security-Policy/i.test(readText(abs) || ''));
|
|
120
|
+
if (any) return [];
|
|
121
|
+
return [{
|
|
122
|
+
ruleId: 'consumer.csp-meta',
|
|
123
|
+
category: 'consumer',
|
|
124
|
+
severity: 'info',
|
|
125
|
+
message: 'No CSP meta found in scanned HTML',
|
|
126
|
+
fix: 'Add Content-Security-Policy header or meta',
|
|
127
|
+
}];
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
registerSecurityRule({
|
|
132
|
+
id: 'consumer.app-debug',
|
|
133
|
+
category: 'consumer',
|
|
134
|
+
severity: 'warning',
|
|
135
|
+
modes: ['consumer', 'audit', 'scan'],
|
|
136
|
+
title: 'Debug flags',
|
|
137
|
+
run(ctx) {
|
|
138
|
+
const findings = [];
|
|
139
|
+
for (const name of ['.env', '.env.example', 'config/app.php', 'velinstyle.config.js']) {
|
|
140
|
+
const t = readText(join(ctx.root, name));
|
|
141
|
+
if (t && /APP_DEBUG\s*=\s*true|debug\s*:\s*true/i.test(t)) {
|
|
142
|
+
findings.push({
|
|
143
|
+
ruleId: 'consumer.app-debug',
|
|
144
|
+
category: 'consumer',
|
|
145
|
+
severity: 'warning',
|
|
146
|
+
message: `Debug flag enabled in ${name}`,
|
|
147
|
+
path: name,
|
|
148
|
+
fix: 'Disable debug in production',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return findings;
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
registerSecurityRule({
|
|
157
|
+
id: 'consumer.source-maps',
|
|
158
|
+
category: 'consumer',
|
|
159
|
+
severity: 'info',
|
|
160
|
+
modes: ['consumer', 'audit'],
|
|
161
|
+
title: 'Public source maps',
|
|
162
|
+
run(ctx) {
|
|
163
|
+
const maps = walkFiles(join(ctx.root, 'dist'), (n) => n.endsWith('.map'), 30);
|
|
164
|
+
if (!maps.length) return [];
|
|
165
|
+
return [{
|
|
166
|
+
ruleId: 'consumer.source-maps',
|
|
167
|
+
category: 'consumer',
|
|
168
|
+
severity: 'info',
|
|
169
|
+
message: `${maps.length} source map(s) under dist/ — ensure intentional for production`,
|
|
170
|
+
path: 'dist/',
|
|
171
|
+
}];
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { registerSecurityRule } from '../../registry.js';
|
|
2
|
+
import { fileExists, readJson, readText, walkFiles, rel } from '../../checks/fs.js';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
const KNOWN_POPULAR = [
|
|
6
|
+
'react', 'vue', 'lodash', 'express', 'webpack', 'typescript', 'eslint', 'vite',
|
|
7
|
+
'next', 'axios', 'jquery', 'moment', 'chalk', 'commander',
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
function levenshtein(a, b) {
|
|
11
|
+
const m = a.length;
|
|
12
|
+
const n = b.length;
|
|
13
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
14
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
15
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
16
|
+
for (let i = 1; i <= m; i++) {
|
|
17
|
+
for (let j = 1; j <= n; j++) {
|
|
18
|
+
dp[i][j] = Math.min(
|
|
19
|
+
dp[i - 1][j] + 1,
|
|
20
|
+
dp[i][j - 1] + 1,
|
|
21
|
+
dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return dp[m][n];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function registerDepsRules() {
|
|
29
|
+
registerSecurityRule({
|
|
30
|
+
id: 'deps.lockfile',
|
|
31
|
+
category: 'deps',
|
|
32
|
+
severity: 'warning',
|
|
33
|
+
modes: ['repo', 'consumer', 'deps', 'audit', 'publish'],
|
|
34
|
+
title: 'Lockfile present',
|
|
35
|
+
run(ctx) {
|
|
36
|
+
const ok =
|
|
37
|
+
fileExists(join(ctx.root, 'package-lock.json'))
|
|
38
|
+
|| fileExists(join(ctx.root, 'pnpm-lock.yaml'))
|
|
39
|
+
|| fileExists(join(ctx.root, 'yarn.lock'));
|
|
40
|
+
if (ok) return [];
|
|
41
|
+
return [{
|
|
42
|
+
ruleId: 'deps.lockfile',
|
|
43
|
+
category: 'deps',
|
|
44
|
+
severity: 'warning',
|
|
45
|
+
message: 'No lockfile found (package-lock / pnpm-lock / yarn.lock)',
|
|
46
|
+
code: 'no-lockfile',
|
|
47
|
+
fix: 'Commit a lockfile for reproducible installs',
|
|
48
|
+
}];
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
registerSecurityRule({
|
|
53
|
+
id: 'deps.lifecycle-hooks',
|
|
54
|
+
category: 'malware',
|
|
55
|
+
severity: 'warning',
|
|
56
|
+
modes: ['repo', 'consumer', 'deps', 'audit', 'publish', 'malware'],
|
|
57
|
+
title: 'Risky lifecycle scripts',
|
|
58
|
+
run(ctx) {
|
|
59
|
+
const pkg = readJson(join(ctx.root, 'package.json'));
|
|
60
|
+
if (!pkg?.scripts) return [];
|
|
61
|
+
const findings = [];
|
|
62
|
+
for (const hook of ['preinstall', 'install', 'postinstall', 'prepublish']) {
|
|
63
|
+
const s = pkg.scripts[hook];
|
|
64
|
+
if (!s) continue;
|
|
65
|
+
if (/curl\s|wget\s|Invoke-WebRequest|fetch\(['"]https?:/i.test(s)) {
|
|
66
|
+
findings.push({
|
|
67
|
+
ruleId: 'deps.lifecycle-hooks',
|
|
68
|
+
category: 'malware',
|
|
69
|
+
severity: 'error',
|
|
70
|
+
message: `Lifecycle script "${hook}" downloads remote content`,
|
|
71
|
+
path: 'package.json',
|
|
72
|
+
code: 'critical',
|
|
73
|
+
fix: 'Remove network installs from lifecycle hooks',
|
|
74
|
+
});
|
|
75
|
+
} else if (/node\s|npx\s|bash\s|sh\s/i.test(s)) {
|
|
76
|
+
findings.push({
|
|
77
|
+
ruleId: 'deps.lifecycle-hooks',
|
|
78
|
+
category: 'malware',
|
|
79
|
+
severity: 'info',
|
|
80
|
+
message: `Lifecycle script "${hook}" runs shell/node — review carefully`,
|
|
81
|
+
path: 'package.json',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return findings;
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
registerSecurityRule({
|
|
90
|
+
id: 'deps.engines',
|
|
91
|
+
category: 'deps',
|
|
92
|
+
severity: 'info',
|
|
93
|
+
modes: ['repo', 'consumer', 'deps', 'audit'],
|
|
94
|
+
title: 'Node engines field',
|
|
95
|
+
run(ctx) {
|
|
96
|
+
const pkg = readJson(join(ctx.root, 'package.json'));
|
|
97
|
+
if (pkg?.engines?.node) return [];
|
|
98
|
+
return [{
|
|
99
|
+
ruleId: 'deps.engines',
|
|
100
|
+
category: 'deps',
|
|
101
|
+
severity: 'info',
|
|
102
|
+
message: 'package.json missing engines.node',
|
|
103
|
+
path: 'package.json',
|
|
104
|
+
fix: 'Declare supported Node versions',
|
|
105
|
+
}];
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
registerSecurityRule({
|
|
110
|
+
id: 'deps.license-field',
|
|
111
|
+
category: 'deps',
|
|
112
|
+
severity: 'warning',
|
|
113
|
+
modes: ['repo', 'deps', 'audit', 'publish'],
|
|
114
|
+
title: 'License field',
|
|
115
|
+
run(ctx) {
|
|
116
|
+
const pkg = readJson(join(ctx.root, 'package.json'));
|
|
117
|
+
if (pkg?.license) return [];
|
|
118
|
+
return [{
|
|
119
|
+
ruleId: 'deps.license-field',
|
|
120
|
+
category: 'deps',
|
|
121
|
+
severity: 'warning',
|
|
122
|
+
message: 'package.json missing license field',
|
|
123
|
+
path: 'package.json',
|
|
124
|
+
code: 'deprecated',
|
|
125
|
+
fix: 'Set license (e.g. MIT)',
|
|
126
|
+
}];
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
registerSecurityRule({
|
|
131
|
+
id: 'deps.typosquat',
|
|
132
|
+
category: 'malware',
|
|
133
|
+
severity: 'warning',
|
|
134
|
+
modes: ['repo', 'consumer', 'deps', 'audit', 'malware'],
|
|
135
|
+
title: 'Typosquat heuristic',
|
|
136
|
+
run(ctx) {
|
|
137
|
+
const pkg = readJson(join(ctx.root, 'package.json'));
|
|
138
|
+
const names = Object.keys({ ...pkg?.dependencies, ...pkg?.devDependencies });
|
|
139
|
+
const findings = [];
|
|
140
|
+
for (const name of names) {
|
|
141
|
+
const base = name.includes('/') ? name.split('/').pop() : name;
|
|
142
|
+
for (const pop of KNOWN_POPULAR) {
|
|
143
|
+
const d = levenshtein(base.toLowerCase(), pop);
|
|
144
|
+
if (d === 1 && base.toLowerCase() !== pop) {
|
|
145
|
+
findings.push({
|
|
146
|
+
ruleId: 'deps.typosquat',
|
|
147
|
+
category: 'malware',
|
|
148
|
+
severity: 'warning',
|
|
149
|
+
message: `Package "${name}" is 1 edit from popular "${pop}" — verify intentional`,
|
|
150
|
+
path: 'package.json',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return findings;
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
registerSecurityRule({
|
|
160
|
+
id: 'deps.deprecated-request',
|
|
161
|
+
category: 'deps',
|
|
162
|
+
severity: 'warning',
|
|
163
|
+
modes: ['repo', 'consumer', 'deps', 'audit'],
|
|
164
|
+
title: 'Known deprecated packages',
|
|
165
|
+
run(ctx) {
|
|
166
|
+
const pkg = readJson(join(ctx.root, 'package.json'));
|
|
167
|
+
const names = Object.keys({ ...pkg?.dependencies, ...pkg?.devDependencies });
|
|
168
|
+
const deprecated = ['request', 'node-uuid', 'crypto', 'gulp-util'];
|
|
169
|
+
return names
|
|
170
|
+
.filter((n) => deprecated.includes(n))
|
|
171
|
+
.map((n) => ({
|
|
172
|
+
ruleId: 'deps.deprecated-request',
|
|
173
|
+
category: 'deps',
|
|
174
|
+
severity: 'warning',
|
|
175
|
+
message: `Dependency "${n}" is widely considered deprecated`,
|
|
176
|
+
path: 'package.json',
|
|
177
|
+
code: 'deprecated',
|
|
178
|
+
fix: 'Replace with maintained alternative',
|
|
179
|
+
}));
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { registerSecretsRules } from './secrets/index.js';
|
|
2
|
+
import { registerDepsRules } from './deps/index.js';
|
|
3
|
+
import { registerCiRules } from './ci/index.js';
|
|
4
|
+
import { registerPublishRules } from './publish/index.js';
|
|
5
|
+
import { registerRepoRules } from './repo/index.js';
|
|
6
|
+
import { registerConsumerRules } from './consumer/index.js';
|
|
7
|
+
|
|
8
|
+
let loaded = false;
|
|
9
|
+
|
|
10
|
+
/** Idempotent: register all built-in Security Engine rules. */
|
|
11
|
+
export function loadBuiltinSecurityRules() {
|
|
12
|
+
if (loaded) return;
|
|
13
|
+
registerSecretsRules();
|
|
14
|
+
registerDepsRules();
|
|
15
|
+
registerCiRules();
|
|
16
|
+
registerPublishRules();
|
|
17
|
+
registerRepoRules();
|
|
18
|
+
registerConsumerRules();
|
|
19
|
+
loaded = true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resetBuiltinSecurityRulesFlag() {
|
|
23
|
+
loaded = false;
|
|
24
|
+
}
|