@nitra/cursor 1.13.1 → 1.13.8
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/.claude-template/hooks/capture-decisions.sh +31 -13
- package/.claude-template/hooks/normalize-decisions.sh +8 -3
- package/CHANGELOG.md +65 -0
- package/bin/n-cursor.js +4 -2
- package/package.json +4 -2
- package/rules/adr/adr.mdc +14 -4
- package/rules/ga/fix/workflows/check.mjs +6 -109
- package/rules/ga/policy/package_json/package_json.rego +24 -0
- package/rules/ga/policy/package_json/target.json +8 -0
- package/rules/ga/policy/vscode_extensions/target.json +8 -0
- package/rules/ga/policy/vscode_extensions/vscode_extensions.rego +16 -0
- package/rules/ga/policy/vscode_settings/target.json +8 -0
- package/rules/ga/policy/vscode_settings/vscode_settings.rego +24 -0
- package/rules/ga/policy/zizmor_yml/target.json +8 -0
- package/rules/ga/policy/zizmor_yml/zizmor_yml.rego +17 -0
- package/rules/js-lint/fix/tooling/check.mjs +6 -83
- package/rules/js-lint/policy/jscpd/jscpd.rego +38 -0
- package/rules/js-lint/policy/jscpd/target.json +8 -0
- package/rules/js-lint/policy/vscode_extensions/target.json +8 -0
- package/rules/js-lint/policy/vscode_extensions/vscode_extensions.rego +25 -0
- package/rules/security/fix/gitleaks/check.mjs +8 -45
- package/rules/security/fix/gitleaks/template/.gitleaks.toml.snippet.toml +12 -0
- package/rules/security/policy/gitleaks/gitleaks.rego +17 -0
- package/rules/security/policy/gitleaks/target.json +8 -0
- package/rules/security/policy/package_json/package_json.rego +22 -59
- package/rules/security/policy/package_json/template/package.json.contains.json +1 -0
- package/rules/security/policy/package_json/template/package.json.deny.json +4 -0
- package/rules/security/policy/package_json/template/package.json.snippet.json +1 -0
- package/rules/security/security.mdc +7 -26
- package/rules/vue/fix/packages/check.mjs +7 -64
- package/rules/vue/policy/package_json/package_json.rego +45 -2
- package/rules/vue/vue.mdc +15 -2
- package/scripts/ensure-nitra-cursor-dev-dependencies.mjs +41 -21
- package/scripts/utils/check-mdc-template-refs.mjs +47 -0
- package/scripts/utils/inline-template-links.mjs +60 -0
- package/scripts/utils/run-conftest-batch.mjs +60 -33
- package/scripts/utils/run-rule.mjs +16 -1
- package/scripts/utils/template.mjs +215 -0
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Перевіряє лінт JavaScript за правилом js-lint.mdc.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Flat ESLint з getConfig і ignore для auto-imports,
|
|
5
5
|
* `.oxlintrc.json` має збігатися з каноном oxlint у пакеті (`npm/scripts/utils/oxlint-canonical.json`):
|
|
6
6
|
* plugins, jsPlugins, categories, усі правила з канону (додаткові записи в `rules` дозволені), settings, env,
|
|
7
|
-
* globals, ignorePatterns.
|
|
8
|
-
* `
|
|
9
|
-
*
|
|
10
|
-
* `@
|
|
11
|
-
* `lint-js.yml`
|
|
12
|
-
* `engines.bun` >= 1.3, `"type": "module"` у кореневому і всіх workspace `package.json`. Дубль перевірки JS у `lint.yml` — заборонено.
|
|
7
|
+
* globals, ignorePatterns. Також перевіряє workspace `package.json` на `type: "module"`
|
|
8
|
+
* і `engines`, workflow-дубль у `lint.yml`, `knip.json` autofill і застарілі `.eslintrc*`.
|
|
9
|
+
*
|
|
10
|
+
* Per-document вимоги (`lint-js`, `@nitra/eslint-config`, root `engines`, `.jscpd.json`,
|
|
11
|
+
* `.vscode/extensions.json`, `lint-js.yml`) — у policy-пакетах `js_lint.*`.
|
|
13
12
|
*/
|
|
14
13
|
import { existsSync } from 'node:fs'
|
|
15
14
|
import { copyFile, readFile } from 'node:fs/promises'
|
|
@@ -42,9 +41,6 @@ export const KNIP_CANONICAL_JSON_PATH = join(
|
|
|
42
41
|
'knip-canonical.json'
|
|
43
42
|
)
|
|
44
43
|
|
|
45
|
-
/** Мінімальні рекомендації розширень редактора з js-lint.mdc (eslint, oxlint, GA). */
|
|
46
|
-
export const REQUIRED_VSCODE_EXTENSIONS = ['dbaeumer.vscode-eslint', 'github.vscode-github-actions', 'oxc.oxc-vscode']
|
|
47
|
-
|
|
48
44
|
const NON_DIGITS_RE = /\D+/u
|
|
49
45
|
|
|
50
46
|
// Канонічний рядок `lint-js`-скрипта і мінімальна версія `@nitra/eslint-config` —
|
|
@@ -353,36 +349,6 @@ async function checkOxlintRc(passFn, failFn) {
|
|
|
353
349
|
}
|
|
354
350
|
}
|
|
355
351
|
|
|
356
|
-
/**
|
|
357
|
-
* Перевіряє .vscode/extensions.json на потрібні розширення.
|
|
358
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
359
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
360
|
-
*/
|
|
361
|
-
async function checkVscodeExtensions(passFn, failFn) {
|
|
362
|
-
if (!existsSync('.vscode/extensions.json')) {
|
|
363
|
-
failFn('.vscode/extensions.json не існує — додай recommendations з js-lint.mdc (див. check-js-lint.mjs)')
|
|
364
|
-
return
|
|
365
|
-
}
|
|
366
|
-
let ext
|
|
367
|
-
try {
|
|
368
|
-
ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
|
|
369
|
-
} catch {
|
|
370
|
-
failFn('.vscode/extensions.json не є валідним JSON')
|
|
371
|
-
return
|
|
372
|
-
}
|
|
373
|
-
const rec = ext.recommendations
|
|
374
|
-
if (!Array.isArray(rec)) {
|
|
375
|
-
failFn('.vscode/extensions.json: поле recommendations має бути масивом')
|
|
376
|
-
return
|
|
377
|
-
}
|
|
378
|
-
const missing = REQUIRED_VSCODE_EXTENSIONS.filter(id => !rec.includes(id))
|
|
379
|
-
if (missing.length > 0) {
|
|
380
|
-
failFn(`.vscode/extensions.json: додай у recommendations: ${missing.join(', ')} (мінімум для js-lint.mdc)`)
|
|
381
|
-
} else {
|
|
382
|
-
passFn('.vscode/extensions.json: є рекомендації oxlint, eslint і GitHub Actions')
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
352
|
/**
|
|
387
353
|
* FS-existence для `lint-js.yml` + cross-file перевірка, що `lint.yml` (якщо існує)
|
|
388
354
|
* не дублює лінт JS-кроки. Структуру `lint-js.yml` (`actions/checkout@v6`,
|
|
@@ -408,47 +374,6 @@ async function checkLintJsWorkflows(passFn, failFn) {
|
|
|
408
374
|
}
|
|
409
375
|
}
|
|
410
376
|
|
|
411
|
-
/**
|
|
412
|
-
* Перевіряє .jscpd.json.
|
|
413
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
414
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
415
|
-
*/
|
|
416
|
-
async function checkJscpdConfig(passFn, failFn) {
|
|
417
|
-
if (!existsSync('.jscpd.json')) {
|
|
418
|
-
failFn('.jscpd.json не існує — створи з полями згідно check js-lint')
|
|
419
|
-
return
|
|
420
|
-
}
|
|
421
|
-
let jscpdCfg
|
|
422
|
-
try {
|
|
423
|
-
jscpdCfg = JSON.parse(await readFile('.jscpd.json', 'utf8'))
|
|
424
|
-
} catch {
|
|
425
|
-
failFn('.jscpd.json не є валідним JSON')
|
|
426
|
-
return
|
|
427
|
-
}
|
|
428
|
-
passFn('.jscpd.json існує')
|
|
429
|
-
if (jscpdCfg.gitignore === true) {
|
|
430
|
-
passFn('.jscpd.json: gitignore увімкнено')
|
|
431
|
-
} else {
|
|
432
|
-
failFn('.jscpd.json має містити "gitignore": true')
|
|
433
|
-
}
|
|
434
|
-
if (jscpdCfg.exitCode === 1) {
|
|
435
|
-
passFn('.jscpd.json: exitCode 1 при дублікатах')
|
|
436
|
-
} else {
|
|
437
|
-
failFn('.jscpd.json має містити "exitCode": 1 (інакше CI не впаде на клонах)')
|
|
438
|
-
}
|
|
439
|
-
if (Array.isArray(jscpdCfg.reporters) && jscpdCfg.reporters.includes('console')) {
|
|
440
|
-
passFn('.jscpd.json: reporters містить console')
|
|
441
|
-
} else {
|
|
442
|
-
failFn('.jscpd.json має містити "reporters": ["console"] (або масив із "console")')
|
|
443
|
-
}
|
|
444
|
-
const minLines = jscpdCfg.minLines
|
|
445
|
-
if (typeof minLines === 'number' && minLines >= 25) {
|
|
446
|
-
passFn(`.jscpd.json: minLines ${minLines} (>=25)`)
|
|
447
|
-
} else {
|
|
448
|
-
failFn('.jscpd.json має містити "minLines" як число >= 25')
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
377
|
/**
|
|
453
378
|
* Перевіряє наявність `knip.json` у корені проєкту. Якщо файл відсутній —
|
|
454
379
|
* копіює канонічний `knip-canonical.json` з пакета `@nitra/cursor` як стартовий
|
|
@@ -485,9 +410,7 @@ export async function check() {
|
|
|
485
410
|
await checkEslintConfig(pass, fail)
|
|
486
411
|
await checkPackageJsonJsLint(pass, fail)
|
|
487
412
|
await checkOxlintRc(pass, fail)
|
|
488
|
-
await checkVscodeExtensions(pass, fail)
|
|
489
413
|
await checkLintJsWorkflows(pass, fail)
|
|
490
|
-
await checkJscpdConfig(pass, fail)
|
|
491
414
|
await checkKnipConfig(pass, fail)
|
|
492
415
|
|
|
493
416
|
for (const dup of ['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.yml']) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Перевірка `.jscpd.json` для js-lint (js-lint.mdc).
|
|
2
|
+
#
|
|
3
|
+
# JS-частина лишається для FS/cross-file: наявність workflow, flat ESLint config,
|
|
4
|
+
# `.oxlintrc.json` проти embedded canonical snapshot, `knip.json` autofill.
|
|
5
|
+
# Цей пакет покриває лише структуру одного JSON-документа.
|
|
6
|
+
#
|
|
7
|
+
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
8
|
+
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
9
|
+
# (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
|
|
10
|
+
package js_lint.jscpd
|
|
11
|
+
|
|
12
|
+
import rego.v1
|
|
13
|
+
|
|
14
|
+
deny contains msg if {
|
|
15
|
+
input.gitignore != true
|
|
16
|
+
msg := ".jscpd.json має містити \"gitignore\": true (js-lint.mdc)"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
deny contains msg if {
|
|
20
|
+
input.exitCode != 1
|
|
21
|
+
msg := ".jscpd.json має містити \"exitCode\": 1 (інакше CI не впаде на клонах) (js-lint.mdc)"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
deny contains msg if {
|
|
25
|
+
not "console" in {r | some r in object.get(input, "reporters", [])}
|
|
26
|
+
msg := ".jscpd.json має містити \"reporters\": [\"console\"] або масив із \"console\" (js-lint.mdc)"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
deny contains msg if {
|
|
30
|
+
not is_number(object.get(input, "minLines", null))
|
|
31
|
+
msg := ".jscpd.json має містити \"minLines\" як число >= 25 (js-lint.mdc)"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
deny contains msg if {
|
|
35
|
+
is_number(input.minLines)
|
|
36
|
+
input.minLines < 25
|
|
37
|
+
msg := ".jscpd.json має містити \"minLines\" як число >= 25 (js-lint.mdc)"
|
|
38
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Перевірка `.vscode/extensions.json` для js-lint (js-lint.mdc).
|
|
2
|
+
#
|
|
3
|
+
# Canonical: у `recommendations` мають бути ESLint, GitHub Actions і Oxlint.
|
|
4
|
+
# Канон задає мінімум — додаткові рекомендації від інших правил дозволені.
|
|
5
|
+
#
|
|
6
|
+
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
7
|
+
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
8
|
+
# (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
|
|
9
|
+
package js_lint.vscode_extensions
|
|
10
|
+
|
|
11
|
+
import rego.v1
|
|
12
|
+
|
|
13
|
+
required_extensions := {
|
|
14
|
+
"dbaeumer.vscode-eslint",
|
|
15
|
+
"github.vscode-github-actions",
|
|
16
|
+
"oxc.oxc-vscode",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
recommendations_set := {r | some r in object.get(input, "recommendations", [])}
|
|
20
|
+
|
|
21
|
+
deny contains msg if {
|
|
22
|
+
some required in required_extensions
|
|
23
|
+
not required in recommendations_set
|
|
24
|
+
msg := sprintf(".vscode/extensions.json: recommendations має містити %q (js-lint.mdc)", [required])
|
|
25
|
+
}
|
|
@@ -1,62 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* FS-частина правила `security
|
|
2
|
+
* FS-частина правила `security`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* - наявність `package.json`
|
|
6
|
-
* -
|
|
7
|
-
* - `.gitleaks.toml` має `useDefault = true` у блоці `[extend]` (інакше дефолтні правила
|
|
8
|
-
* gitleaks перетираються і скан стає сліпим до 95% типових витоків).
|
|
4
|
+
* Перевіряє:
|
|
5
|
+
* - наявність `package.json` (структуру валідує Rego);
|
|
6
|
+
* - контекстне pass-повідомлення для JS-концерну.
|
|
9
7
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* - агрегований `scripts.lint` (якщо є) містить `bun run lint-security`;
|
|
13
|
-
* - `gitleaks` НЕ у `dependencies` / `devDependencies` (бо це глобальний CLI).
|
|
8
|
+
* Наявність і вміст `.gitleaks.toml` (`[extend].useDefault = true`) тепер
|
|
9
|
+
* перевіряє policy `security.gitleaks`.
|
|
14
10
|
*/
|
|
15
11
|
import { existsSync } from 'node:fs'
|
|
16
|
-
import { readFile } from 'node:fs/promises'
|
|
17
12
|
|
|
18
13
|
import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
|
|
19
14
|
|
|
20
|
-
const GITLEAKS_CONFIG = '.gitleaks.toml'
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Перевіряє наявність `.gitleaks.toml` у корені та канонічну вимогу `useDefault = true`
|
|
24
|
-
* у блоці `[extend]`. Користувач сам наповнює `[allowlist]` локальними патернами.
|
|
25
|
-
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
26
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
27
|
-
* @returns {Promise<void>}
|
|
28
|
-
*/
|
|
29
|
-
async function checkGitleaksConfig(pass, fail) {
|
|
30
|
-
if (!existsSync(GITLEAKS_CONFIG)) {
|
|
31
|
-
fail(`${GITLEAKS_CONFIG} не знайдено в корені — створи за каноном security.mdc (useDefault = true + [allowlist])`)
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
|
-
const raw = await readFile(GITLEAKS_CONFIG, 'utf8')
|
|
35
|
-
if (!/useDefault\s*=\s*true/u.test(raw)) {
|
|
36
|
-
fail(
|
|
37
|
-
`${GITLEAKS_CONFIG}: відсутнє \`useDefault = true\` у блоці [extend] — без нього вбудовані ` +
|
|
38
|
-
'gitleaks-правила перетираються і скан стає сліпим (security.mdc)'
|
|
39
|
-
)
|
|
40
|
-
return
|
|
41
|
-
}
|
|
42
|
-
pass(`${GITLEAKS_CONFIG} існує і успадковує дефолтні gitleaks-правила (useDefault = true)`)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Запускає всі FS-перевірки правила security.
|
|
47
|
-
* @returns {Promise<number>} 0 — все OK, 1 — є зауваження
|
|
48
|
-
*/
|
|
49
15
|
export async function check() {
|
|
50
16
|
const reporter = createCheckReporter()
|
|
51
17
|
const { pass, fail } = reporter
|
|
52
|
-
|
|
53
18
|
if (!existsSync('package.json')) {
|
|
54
19
|
fail('package.json не знайдено в корені — додай (security.mdc)')
|
|
55
20
|
return reporter.getExitCode()
|
|
56
21
|
}
|
|
57
|
-
pass('package.json є (структуру перевіряє
|
|
58
|
-
|
|
59
|
-
await checkGitleaksConfig(pass, fail)
|
|
60
|
-
|
|
22
|
+
pass('package.json є (структуру перевіряє Rego)')
|
|
23
|
+
pass('.gitleaks.toml перевіряє npx @nitra/cursor check → security.gitleaks')
|
|
61
24
|
return reporter.getExitCode()
|
|
62
25
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Перевірка `.gitleaks.toml` для security (security.mdc).
|
|
2
|
+
#
|
|
3
|
+
# Канонічна мінімальна вимога: `[extend].useDefault = true`, щоб локальний
|
|
4
|
+
# конфіг не вимикав стандартні правила gitleaks. Додаткові локальні правила
|
|
5
|
+
# дозволені.
|
|
6
|
+
#
|
|
7
|
+
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
8
|
+
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
9
|
+
# (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
|
|
10
|
+
package security.gitleaks
|
|
11
|
+
|
|
12
|
+
import rego.v1
|
|
13
|
+
|
|
14
|
+
deny contains msg if {
|
|
15
|
+
object.get(object.get(input, "extend", {}), "useDefault", null) != true
|
|
16
|
+
msg := ".gitleaks.toml: [extend].useDefault має бути true (security.mdc)"
|
|
17
|
+
}
|
|
@@ -1,75 +1,38 @@
|
|
|
1
1
|
# Перевірка `package.json` для правила security (security.mdc).
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
# conftest test package.json -p npm/policy/security \
|
|
5
|
-
# --namespace security.package_json
|
|
6
|
-
#
|
|
7
|
-
# Перевіряє: наявність `scripts.lint-security`, виклик `gitleaks detect`,
|
|
8
|
-
# входження `bun run lint-security` у агрегований `scripts.lint` (якщо `lint` є),
|
|
9
|
-
# та заборону `gitleaks` у dependencies/devDependencies (інструмент глобальний).
|
|
10
|
-
#
|
|
11
|
-
# FS-перевірки (наявність `.gitleaks.toml`, `useDefault = true`) — у JS.
|
|
12
|
-
#
|
|
13
|
-
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
14
|
-
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
15
|
-
# (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
|
|
2
|
+
# Канон надходить через --data: { "template": { "snippet": ..., "deny": ..., "contains": ... } }
|
|
3
|
+
# Структура --data сформована з template/<target>.{snippet,deny,contains}.json концерну.
|
|
16
4
|
package security.package_json
|
|
17
5
|
|
|
18
6
|
import rego.v1
|
|
19
7
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
dep_template := concat(" ", [
|
|
23
|
-
"package.json: %q не повинен бути в %s —",
|
|
24
|
-
"gitleaks встановлюється глобально (security.mdc)",
|
|
25
|
-
])
|
|
26
|
-
|
|
27
|
-
# ── deny: scripts.lint-security ──────────────────────────────────────────
|
|
28
|
-
|
|
8
|
+
# ── deny: кожен snippet leaf має співпадати з input ──────────────────────────
|
|
29
9
|
deny contains msg if {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
10
|
+
some script_name, expected in data.template.snippet.scripts
|
|
11
|
+
actual := object.get(object.get(input, "scripts", {}), script_name, "")
|
|
12
|
+
actual != expected
|
|
13
|
+
msg := sprintf("package.json: scripts.%s має бути %q (security.mdc)", [script_name, expected])
|
|
33
14
|
}
|
|
34
15
|
|
|
16
|
+
# ── deny: жодного ключа з deny у dependencies/devDependencies ────────────────
|
|
35
17
|
deny contains msg if {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
msg := "package.json: lint-security має викликати `gitleaks` (security.mdc)"
|
|
18
|
+
some pkg, reason in data.template.deny.dependencies
|
|
19
|
+
pkg in object.keys(object.get(input, "dependencies", {}))
|
|
20
|
+
msg := sprintf("package.json: dependencies.%s — %s (security.mdc)", [pkg, reason])
|
|
40
21
|
}
|
|
41
22
|
|
|
42
23
|
deny contains msg if {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
msg := "package.json: lint-security має містити `detect` або `git` як gitleaks-subcommand (security.mdc)"
|
|
24
|
+
some pkg, reason in data.template.deny.devDependencies
|
|
25
|
+
pkg in object.keys(object.get(input, "devDependencies", {}))
|
|
26
|
+
msg := sprintf("package.json: devDependencies.%s — %s (security.mdc)", [pkg, reason])
|
|
47
27
|
}
|
|
48
28
|
|
|
49
|
-
# ── deny:
|
|
50
|
-
|
|
29
|
+
# ── deny: рядкові поля з contains мають містити кожен substring ──────────────
|
|
30
|
+
# Перевіряємо лише наявні поля (якщо `scripts.<name>` відсутній — поле опціональне).
|
|
51
31
|
deny contains msg if {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
32
|
+
some script_name, needles in data.template.contains.scripts
|
|
33
|
+
actual := object.get(object.get(input, "scripts", {}), script_name, "")
|
|
34
|
+
actual != ""
|
|
35
|
+
some needle in needles
|
|
36
|
+
not contains(actual, needle)
|
|
37
|
+
msg := sprintf("package.json: scripts.%s має містити %q (security.mdc)", [script_name, needle])
|
|
57
38
|
}
|
|
58
|
-
|
|
59
|
-
# ── deny: `gitleaks` НЕ в dependencies/devDependencies ───────────────────
|
|
60
|
-
|
|
61
|
-
deny contains msg if {
|
|
62
|
-
gitleaks_pkg in object.keys(object.get(input, "dependencies", {}))
|
|
63
|
-
msg := sprintf(dep_template, [gitleaks_pkg, "dependencies"])
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
deny contains msg if {
|
|
67
|
-
gitleaks_pkg in object.keys(object.get(input, "devDependencies", {}))
|
|
68
|
-
msg := sprintf(dep_template, [gitleaks_pkg, "devDependencies"])
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
# ── helpers ──────────────────────────────────────────────────────────────
|
|
72
|
-
|
|
73
|
-
# Чи містить рядок subcommand `detect` або `git` (як слово, не як підрядок випадкового шляху).
|
|
74
|
-
# `gitleaks detect ...`, `gitleaks git --no-banner`, `gitleaks detect --source=.` — усі OK.
|
|
75
|
-
has_detect_or_git_subcommand(s) if regex.match(`\bgitleaks\s+(detect|git)\b`, s)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "scripts": { "lint": ["bun run lint-security"] } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "scripts": { "lint-security": "gitleaks detect --no-banner" } }
|
|
@@ -9,14 +9,11 @@ version: '1.1'
|
|
|
9
9
|
|
|
10
10
|
## Канон `package.json#scripts`
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
```
|
|
12
|
+
- `lint-security` скрипт: [package.json.snippet.json](./policy/package_json/template/package.json.snippet.json)
|
|
13
|
+
- `lint` агрегатор повинен містити: [package.json.contains.json](./policy/package_json/template/package.json.contains.json)
|
|
14
|
+
- Заборонено `gitleaks` у `dependencies`/`devDependencies`: [package.json.deny.json](./policy/package_json/template/package.json.deny.json)
|
|
15
|
+
|
|
16
|
+
**Зауваження:**
|
|
20
17
|
|
|
21
18
|
- `gitleaks detect` — сканує робоче дерево (uncommitted + tracked); швидше і безпечніше для частого `bun run lint`, ніж `gitleaks git`.
|
|
22
19
|
- `--no-banner` — прибирає ASCII-арт (CI-friendly).
|
|
@@ -24,25 +21,9 @@ version: '1.1'
|
|
|
24
21
|
|
|
25
22
|
## `.gitleaks.toml` (рекомендована основа)
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
title = "Project gitleaks config"
|
|
29
|
-
|
|
30
|
-
[extend]
|
|
31
|
-
useDefault = true
|
|
32
|
-
|
|
33
|
-
[allowlist]
|
|
34
|
-
description = "Файли й шляхи, які навмисно містять test-фікстури з паттернами секретів."
|
|
35
|
-
paths = [
|
|
36
|
-
'''(^|/)node_modules(/|$)''',
|
|
37
|
-
'''(^|/)\.git(/|$)''',
|
|
38
|
-
'''(^|/)dist(/|$)''',
|
|
39
|
-
'''(^|/)build(/|$)''',
|
|
40
|
-
'''.*\.lock$''',
|
|
41
|
-
'''.*fixtures?/.*'''
|
|
42
|
-
]
|
|
43
|
-
```
|
|
24
|
+
Канон (допускає розширення в `[allowlist].paths`): [.gitleaks.toml.snippet.toml](./fix/gitleaks/template/.gitleaks.toml.snippet.toml)
|
|
44
25
|
|
|
45
|
-
`useDefault = true` — НЕ перетирай дефолтний `rules`-масив; реальні винятки лише через `[allowlist]`.
|
|
26
|
+
**Важливо:** `useDefault = true` — НЕ перетирай дефолтний `rules`-масив; реальні винятки лише через `[allowlist]`.
|
|
46
27
|
|
|
47
28
|
## GitHub Actions (опційно)
|
|
48
29
|
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Знаходить пакети з `vue` у dependencies і перевіряє їх за правилом vue.mdc.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Вміст `vite.config`, editor-файли для Vite client types, у репозиторії —
|
|
5
|
+
* рекомендацію розширення Vue.volar.
|
|
6
|
+
*
|
|
7
|
+
* Залежності Vue/Vite (`vite >= 8`, `@vitejs/plugin-vue`, `vue-macros`,
|
|
8
|
+
* `unplugin-auto-import`, `vite-plugin-vue-layouts-next`, заборона `esbuild`) —
|
|
9
|
+
* у policy `vue.package_json`.
|
|
6
10
|
*
|
|
7
11
|
* У кожному Vue+Vite-пакеті очікується `src/vite-env.d.ts` з `/// <reference types="vite/client" />`
|
|
8
12
|
* та `jsconfig.json` у корені пакета (типи для імпортів асетів у `.vue`).
|
|
@@ -183,30 +187,6 @@ function ukFilesCountPhrase(n) {
|
|
|
183
187
|
return `${n} файлів`
|
|
184
188
|
}
|
|
185
189
|
|
|
186
|
-
/**
|
|
187
|
-
* Перевіряє наявність залежності в об'єкті deps.
|
|
188
|
-
* @param {Record<string,string>} deps об'єкт залежностей
|
|
189
|
-
* @param {string} name ім'я пакета
|
|
190
|
-
* @param {string} prefix префікс повідомлення
|
|
191
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
192
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
193
|
-
* @param {string} hint підказка при відсутності
|
|
194
|
-
*/
|
|
195
|
-
function checkRequiredDep(deps, name, prefix, passFn, fail, hint = `${name} відсутній`) {
|
|
196
|
-
if (deps[name]) {
|
|
197
|
-
passFn(`${prefix}${name}: ${deps[name]}`)
|
|
198
|
-
} else {
|
|
199
|
-
fail(`${prefix}${hint}`)
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Перевіряє версію vite у devDependencies.
|
|
205
|
-
* @param {Record<string,string>} devDeps devDependencies з package.json
|
|
206
|
-
* @param {string} prefix параметр prefix
|
|
207
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
208
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
209
|
-
*/
|
|
210
190
|
/**
|
|
211
191
|
* Перевіряє `src/vite-env.d.ts` і наявність `jsconfig.json` для підтягування типів асетів Vite у IDE.
|
|
212
192
|
* @param {string} rootDir відносний шлях до кореня пакета
|
|
@@ -433,44 +413,7 @@ async function checkVueImportViolations(rootDir, absPackageRoot, ignorePaths, ha
|
|
|
433
413
|
*/
|
|
434
414
|
async function checkVuePackage(rootDir, ignorePaths, fail, passFn) {
|
|
435
415
|
const prefix = `[${packageLabel(rootDir)}] `
|
|
436
|
-
|
|
437
|
-
const deps = pkg.dependencies || {}
|
|
438
|
-
const devDeps = pkg.devDependencies || {}
|
|
439
|
-
const allDeps = { ...deps, ...devDeps }
|
|
440
|
-
|
|
441
|
-
if (allDeps.esbuild) {
|
|
442
|
-
fail(`${prefix}esbuild заборонено (знайдено: ${allDeps.esbuild}). Замінити на rolldown та прибрати esbuild.`)
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
checkRequiredDep(deps, 'vue', prefix, passFn, fail, 'vue відсутній в dependencies')
|
|
446
|
-
// `vite >= 8` перенесено в Rego (`npm/policy/vue/package_json/`); запуск
|
|
447
|
-
// через `npx @nitra/cursor check`. Залишені вимоги — present-/missing-deps
|
|
448
|
-
// (vue-macros, unplugin-auto-import, тощо) — у самому JS-чекері.
|
|
449
|
-
checkRequiredDep(
|
|
450
|
-
devDeps,
|
|
451
|
-
'@vitejs/plugin-vue',
|
|
452
|
-
prefix,
|
|
453
|
-
passFn,
|
|
454
|
-
fail,
|
|
455
|
-
'@vitejs/plugin-vue відсутній в devDependencies'
|
|
456
|
-
)
|
|
457
|
-
checkRequiredDep(allDeps, 'vue-macros', prefix, passFn, fail, 'vue-macros відсутній — bun add -d vue-macros')
|
|
458
|
-
checkRequiredDep(
|
|
459
|
-
allDeps,
|
|
460
|
-
'unplugin-auto-import',
|
|
461
|
-
prefix,
|
|
462
|
-
passFn,
|
|
463
|
-
fail,
|
|
464
|
-
'unplugin-auto-import відсутній — bun add -d unplugin-auto-import'
|
|
465
|
-
)
|
|
466
|
-
checkRequiredDep(
|
|
467
|
-
allDeps,
|
|
468
|
-
'vite-plugin-vue-layouts-next',
|
|
469
|
-
prefix,
|
|
470
|
-
passFn,
|
|
471
|
-
fail,
|
|
472
|
-
'vite-plugin-vue-layouts-next відсутній — bun add -d vite-plugin-vue-layouts-next'
|
|
473
|
-
)
|
|
416
|
+
passFn(`${prefix}package.json залежності перевіряє npx @nitra/cursor check → vue.package_json`)
|
|
474
417
|
|
|
475
418
|
await checkViteClientEnvAndEditorConfig(rootDir, prefix, passFn, fail)
|
|
476
419
|
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
# conftest test path/to/package.json -p npm/policy/vue \
|
|
6
6
|
# --namespace vue.package_json
|
|
7
7
|
#
|
|
8
|
-
# Перевіряє: якщо в `dependencies` є `vue`, то
|
|
9
|
-
#
|
|
8
|
+
# Перевіряє: якщо в `dependencies` є `vue`, то потрібні канонічні Vue/Vite
|
|
9
|
+
# залежності, `devDependencies.vite` має бути мажорної версії ≥ 8, а `esbuild`
|
|
10
|
+
# у dependencies/devDependencies заборонений (міграція на rolldown).
|
|
10
11
|
#
|
|
11
12
|
# AST-сканування коду (заборона явних value-імпортів `from 'vue'`, заборона
|
|
12
13
|
# Node-нативних модулів у `.vue` SFC, перевірка `vite.config` на
|
|
@@ -34,12 +35,54 @@ deny contains msg if {
|
|
|
34
35
|
msg := sprintf("Vue-пакет: vite має бути >= 8 (зараз %q) (vue.mdc)", [vite_range])
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
deny contains msg if {
|
|
39
|
+
uses_vue
|
|
40
|
+
not "@vitejs/plugin-vue" in dev_dependency_names
|
|
41
|
+
msg := "Vue-пакет: @vitejs/plugin-vue відсутній у devDependencies (vue.mdc)"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
deny contains msg if {
|
|
45
|
+
uses_vue
|
|
46
|
+
not "vue-macros" in all_dependency_names
|
|
47
|
+
msg := "Vue-пакет: vue-macros відсутній (vue.mdc)"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
deny contains msg if {
|
|
51
|
+
uses_vue
|
|
52
|
+
not "unplugin-auto-import" in all_dependency_names
|
|
53
|
+
msg := "Vue-пакет: unplugin-auto-import відсутній (vue.mdc)"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
deny contains msg if {
|
|
57
|
+
uses_vue
|
|
58
|
+
not "vite-plugin-vue-layouts-next" in all_dependency_names
|
|
59
|
+
msg := "Vue-пакет: vite-plugin-vue-layouts-next відсутній (vue.mdc)"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
deny contains msg if {
|
|
63
|
+
uses_vue
|
|
64
|
+
"esbuild" in all_dependency_names
|
|
65
|
+
msg := "Vue-пакет: esbuild заборонено — заміни на rolldown і прибери залежність (vue.mdc)"
|
|
66
|
+
}
|
|
67
|
+
|
|
37
68
|
# ── helpers ────────────────────────────────────────────────────────────────
|
|
38
69
|
|
|
39
70
|
uses_vue if {
|
|
40
71
|
"vue" in object.keys(object.get(input, "dependencies", {}))
|
|
41
72
|
}
|
|
42
73
|
|
|
74
|
+
dependency_names := {n | some n in object.keys(object.get(input, "dependencies", {}))}
|
|
75
|
+
|
|
76
|
+
dev_dependency_names := {n | some n in object.keys(object.get(input, "devDependencies", {}))}
|
|
77
|
+
|
|
78
|
+
all_dependency_names contains n if {
|
|
79
|
+
some n in dependency_names
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
all_dependency_names contains n if {
|
|
83
|
+
some n in dev_dependency_names
|
|
84
|
+
}
|
|
85
|
+
|
|
43
86
|
vite_in_dev_dependencies if {
|
|
44
87
|
"vite" in object.keys(object.get(input, "devDependencies", {}))
|
|
45
88
|
}
|