@nitra/cursor 1.13.52 → 1.13.57
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/CHANGELOG.md +30 -0
- package/package.json +1 -1
- package/rules/image-avif/fix/avif_generation/check.mjs +1 -3
- package/rules/js-bun-db/fix/safety/check.mjs +175 -16
- package/rules/js-bun-db/js-bun-db.mdc +194 -7
- package/rules/js-bun-db/policy/package_json/template/package.json.deny.json +0 -1
- package/rules/k8s/fix/manifests/check.mjs +29 -92
- package/rules/k8s/k8s.mdc +27 -16
- package/rules/k8s/lint/lint.mjs +4 -4
- package/rules/k8s/policy/base_kustomization/base_kustomization.rego +9 -9
- package/rules/k8s/policy/base_manifest/target.json +1 -1
- package/rules/k8s/policy/gateway/target.json +1 -1
- package/rules/k8s/policy/hpa_pdb/target.json +1 -1
- package/rules/k8s/policy/manifest/target.json +1 -1
- package/scripts/utils/bun-sql-scan.mjs +233 -0
|
@@ -31,6 +31,16 @@ import {
|
|
|
31
31
|
|
|
32
32
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
33
33
|
const BUN_SQL_IMPORT_RE = /\bimport\s*\{[\s\S]*?\b(sql|SQL)\b[\s\S]*?\}\s*from\s*["']bun["']/u
|
|
34
|
+
// Імпорт із npm-пакета `pg` — будь-яка з форм: default, named, namespace, side-effect,
|
|
35
|
+
// а також `require('pg')`. `pg-format`/`pg-pool` свідомо НЕ матчаться: на них діє
|
|
36
|
+
// окрема заборона (denylist) і свої повідомлення. Виключення для LISTEN/NOTIFY
|
|
37
|
+
// стосується лише самого клієнта `pg`.
|
|
38
|
+
const PG_LIB_IMPORT_RE = /(?:\bimport\b[\s\S]*?\bfrom\s*["']pg["']|\brequire\s*\(\s*["']pg["']\s*\))/u
|
|
39
|
+
// Першоквазі-рядок або string literal, що починається з LISTEN / UNLISTEN / NOTIFY
|
|
40
|
+
// (case-insensitive), з опційним leading whitespace. Сигнал, що в коді запит
|
|
41
|
+
// `LISTEN ch` / `NOTIFY ch, 'msg'` / `UNLISTEN *` — це функціонал, якого Bun SQL
|
|
42
|
+
// поки не має, тож у проекті лишається легітимна потреба у клієнті `pg`.
|
|
43
|
+
const PG_LISTEN_NOTIFY_SQL_RE = /^\s*(LISTEN|UNLISTEN|NOTIFY)\b/iu
|
|
34
44
|
const IN_PLACEHOLDER_END_RE = /\bin\s*(\(\s*)?$/iu
|
|
35
45
|
// `// allow-unsafe: <reason>` — `allow-unsafe`, двокрапка, **непорожня** причина.
|
|
36
46
|
// Без причини маркер не приймається: ціль — лишити слід для ревʼюера, а не «німий» прапорець.
|
|
@@ -531,6 +541,43 @@ export function findBunSqlUnsafeUseWithoutAllowMarkerInText(content, virtualPath
|
|
|
531
541
|
return out
|
|
532
542
|
}
|
|
533
543
|
|
|
544
|
+
/**
|
|
545
|
+
* Знаходить `<obj>.unsafe(template_literal_with_interpolation)` — навіть із маркером
|
|
546
|
+
* `// allow-unsafe`. Шаблонна підстановка `${name}` у `sql.unsafe`-рядок **не екранує**
|
|
547
|
+
* identifier'ів (reserved words, спецсимволи) і ніяк не біндить значення — це
|
|
548
|
+
* структурна injection-поверхня, яку легко не помітити в ревʼю. Канон — побудувати
|
|
549
|
+
* `text` через `@scaleleap/pg-format` `format('%I', name)` (для identifiers) або
|
|
550
|
+
* звичайні позиційні `$N`-placeholder'и (для values), і передати в `sql.unsafe(text, [params])`.
|
|
551
|
+
*
|
|
552
|
+
* Прапорує саме `TemplateLiteral` з `expressions.length > 0`; статичні рядки
|
|
553
|
+
* (`Literal`, `StringLiteral`, `TemplateLiteral` без `${...}`) і виклики з готовим
|
|
554
|
+
* `text` як змінною — не зачіпає (для них діє основна перевірка allow-unsafe).
|
|
555
|
+
* @param {string} content вихідний код
|
|
556
|
+
* @param {string} [virtualPath] шлях для вибору `lang`
|
|
557
|
+
* @returns {{ line: number, snippet: string }[]} список порушень
|
|
558
|
+
*/
|
|
559
|
+
export function findBunSqlUnsafeWithInterpolatedTemplateInText(content, virtualPath = 'scan.ts') {
|
|
560
|
+
const program = parseProgramOrNull(content, virtualPath)
|
|
561
|
+
if (!program) return []
|
|
562
|
+
|
|
563
|
+
/** @type {{ line: number, snippet: string }[]} */
|
|
564
|
+
const out = []
|
|
565
|
+
walkAstWithAncestors(program, [], node => {
|
|
566
|
+
if (!isUnsafeCall(node)) return
|
|
567
|
+
const args = node.arguments
|
|
568
|
+
if (!Array.isArray(args) || args.length === 0) return
|
|
569
|
+
const first = args[0]
|
|
570
|
+
if (!first || first.type !== 'TemplateLiteral') return
|
|
571
|
+
const expressions = first.expressions
|
|
572
|
+
if (!Array.isArray(expressions) || expressions.length === 0) return
|
|
573
|
+
out.push({
|
|
574
|
+
line: offsetToLine(content, node.start),
|
|
575
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end))
|
|
576
|
+
})
|
|
577
|
+
})
|
|
578
|
+
return out
|
|
579
|
+
}
|
|
580
|
+
|
|
534
581
|
/**
|
|
535
582
|
* Знаходить pg-leftover виклики `<obj>.connect(...)` / `<obj>.end(...)` без маркера
|
|
536
583
|
* `// allow-pg-leftover: <reason>` у файлах, де **в цьому ж файлі** є `import { sql|SQL } from 'bun'`.
|
|
@@ -685,6 +732,192 @@ export function textHasBunSqlImport(content) {
|
|
|
685
732
|
return BUN_SQL_IMPORT_RE.test(content)
|
|
686
733
|
}
|
|
687
734
|
|
|
735
|
+
/**
|
|
736
|
+
* Чи імпортує файл npm-пакет `pg` (`import ... from 'pg'` або `require('pg')`).
|
|
737
|
+
* Текстова перевірка — без AST, дешевий pre-filter; для строгої локалізації
|
|
738
|
+
* (рядок/snippet) використай `findPgLibImportInText`. Не матчить `pg-format`,
|
|
739
|
+
* `pg-pool`, `@types/pg` — лише сам клієнт.
|
|
740
|
+
* @param {string} content вміст файлу
|
|
741
|
+
* @returns {boolean} true, якщо у файлі є імпорт `'pg'`
|
|
742
|
+
*/
|
|
743
|
+
export function textHasPgLibImport(content) {
|
|
744
|
+
return PG_LIB_IMPORT_RE.test(content)
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Знаходить ImportDeclaration / CallExpression `require('pg')` для пакета `pg`
|
|
749
|
+
* (саме точна назва, не `pg-format` тощо). Повертає рядок і snippet — щоб у
|
|
750
|
+
* повідомленнях `fail` показати конкретне місце.
|
|
751
|
+
* @param {string} content вихідний код
|
|
752
|
+
* @param {string} [virtualPath] шлях для вибору `lang`
|
|
753
|
+
* @returns {{ line: number, snippet: string }[]} список місць, де імпортується `pg`
|
|
754
|
+
*/
|
|
755
|
+
export function findPgLibImportInText(content, virtualPath = 'scan.ts') {
|
|
756
|
+
const program = parseProgramOrNull(content, virtualPath)
|
|
757
|
+
if (!program) return []
|
|
758
|
+
|
|
759
|
+
/** @type {{ line: number, snippet: string }[]} */
|
|
760
|
+
const out = []
|
|
761
|
+
walkAstWithAncestors(program, [], node => {
|
|
762
|
+
if (node.type === 'ImportDeclaration' && getStringLiteralValue(node.source) === 'pg') {
|
|
763
|
+
out.push({
|
|
764
|
+
line: offsetToLine(content, node.start),
|
|
765
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end))
|
|
766
|
+
})
|
|
767
|
+
return
|
|
768
|
+
}
|
|
769
|
+
if (node.type === 'CallExpression' && isRequireOfModule(node, 'pg')) {
|
|
770
|
+
out.push({
|
|
771
|
+
line: offsetToLine(content, node.start),
|
|
772
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end))
|
|
773
|
+
})
|
|
774
|
+
}
|
|
775
|
+
})
|
|
776
|
+
return out
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Знаходить використання PostgreSQL LISTEN/NOTIFY у коді — сигнал, що проект
|
|
781
|
+
* потребує `pg` як виняток (Bun SQL поки не реалізує LISTEN/NOTIFY). Прапорує:
|
|
782
|
+
* - `<obj>.query(...)` / `<obj>.queryArray(...)` / `<obj>.queryStream(...)`, де
|
|
783
|
+
* перший аргумент — string literal або template literal, що починається з
|
|
784
|
+
* `LISTEN ` / `UNLISTEN ` / `NOTIFY ` (case-insensitive);
|
|
785
|
+
* - `<obj>.on('notification', ...)` — pg-listener notification-подій (другий
|
|
786
|
+
* аргумент — функція; перший — точно рядок `'notification'`);
|
|
787
|
+
* - TaggedTemplateExpression виду `sql\`LISTEN ...\`` — на випадок, якщо хтось
|
|
788
|
+
* використовує Bun SQL-tagged-template, а LISTEN/NOTIFY все одно лишається у
|
|
789
|
+
* тексті запиту (це не запрацює у Bun SQL, але як сигнал — приймаємо).
|
|
790
|
+
*
|
|
791
|
+
* Регістр SQL-слів не важливий, провідні пробіли допускаються.
|
|
792
|
+
* @param {string} content вихідний код
|
|
793
|
+
* @param {string} [virtualPath] шлях для вибору `lang`
|
|
794
|
+
* @returns {{ line: number, snippet: string, kind: 'listen_sql' | 'notify_sql' | 'unlisten_sql' | 'notification_listener' }[]} список знахідок
|
|
795
|
+
*/
|
|
796
|
+
export function findPgListenNotifyUsageInText(content, virtualPath = 'scan.ts') {
|
|
797
|
+
const program = parseProgramOrNull(content, virtualPath)
|
|
798
|
+
if (!program) return []
|
|
799
|
+
|
|
800
|
+
/** @type {{ line: number, snippet: string, kind: 'listen_sql' | 'notify_sql' | 'unlisten_sql' | 'notification_listener' }[]} */
|
|
801
|
+
const out = []
|
|
802
|
+
walkAstWithAncestors(program, [], node => {
|
|
803
|
+
const fromCall = listenNotifyFromCallExpression(node)
|
|
804
|
+
if (fromCall) {
|
|
805
|
+
out.push({
|
|
806
|
+
line: offsetToLine(content, node.start),
|
|
807
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end)),
|
|
808
|
+
kind: fromCall
|
|
809
|
+
})
|
|
810
|
+
return
|
|
811
|
+
}
|
|
812
|
+
const fromTagged = listenNotifyFromTaggedTemplate(node)
|
|
813
|
+
if (fromTagged) {
|
|
814
|
+
out.push({
|
|
815
|
+
line: offsetToLine(content, node.start),
|
|
816
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end)),
|
|
817
|
+
kind: fromTagged
|
|
818
|
+
})
|
|
819
|
+
}
|
|
820
|
+
})
|
|
821
|
+
return out
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* @param {Record<string, unknown>} node ImportDeclaration.source або CallExpression.arguments[0]
|
|
826
|
+
* @returns {string | null} значення string literal або null
|
|
827
|
+
*/
|
|
828
|
+
function getStringLiteralValue(node) {
|
|
829
|
+
if (!node || typeof node !== 'object') return null
|
|
830
|
+
if ((node.type === 'Literal' || node.type === 'StringLiteral') && typeof node.value === 'string') {
|
|
831
|
+
return node.value
|
|
832
|
+
}
|
|
833
|
+
return null
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Чи це `require('<moduleName>')` — CallExpression з callee Identifier `require`
|
|
838
|
+
* і єдиним string-літералом-аргументом.
|
|
839
|
+
* @param {Record<string, unknown>} node CallExpression
|
|
840
|
+
* @param {string} moduleName очікувана назва модуля (точне співпадіння)
|
|
841
|
+
* @returns {boolean} true, якщо це саме require цього модуля
|
|
842
|
+
*/
|
|
843
|
+
function isRequireOfModule(node, moduleName) {
|
|
844
|
+
const callee = node.callee
|
|
845
|
+
if (!callee || callee.type !== 'Identifier' || callee.name !== 'require') return false
|
|
846
|
+
const args = node.arguments
|
|
847
|
+
if (!Array.isArray(args) || args.length !== 1) return false
|
|
848
|
+
return getStringLiteralValue(args[0]) === moduleName
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Аналізує CallExpression на предмет pg-style LISTEN/NOTIFY-запиту або listener'а
|
|
853
|
+
* подій `'notification'`. Повертає тип сигналу або `null`.
|
|
854
|
+
* @param {Record<string, unknown>} node AST node
|
|
855
|
+
* @returns {'listen_sql' | 'notify_sql' | 'unlisten_sql' | 'notification_listener' | null} kind знахідки
|
|
856
|
+
*/
|
|
857
|
+
function listenNotifyFromCallExpression(node) {
|
|
858
|
+
if (!node || node.type !== 'CallExpression') return null
|
|
859
|
+
const callee = node.callee
|
|
860
|
+
if (!callee || callee.type !== 'MemberExpression' || callee.computed) return null
|
|
861
|
+
const prop = callee.property
|
|
862
|
+
if (!prop || prop.type !== 'Identifier' || typeof prop.name !== 'string') return null
|
|
863
|
+
const args = node.arguments
|
|
864
|
+
if (!Array.isArray(args) || args.length === 0) return null
|
|
865
|
+
|
|
866
|
+
if (prop.name === 'on') {
|
|
867
|
+
return getStringLiteralValue(args[0]) === 'notification' ? 'notification_listener' : null
|
|
868
|
+
}
|
|
869
|
+
if (prop.name === 'query' || prop.name === 'queryArray' || prop.name === 'queryStream') {
|
|
870
|
+
return sqlStringStartsWithListenNotify(args[0])
|
|
871
|
+
}
|
|
872
|
+
return null
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Аналізує TaggedTemplateExpression `<tag>\`LISTEN ...\``: якщо перший quasi
|
|
877
|
+
* починається з LISTEN/UNLISTEN/NOTIFY — повертає відповідний kind.
|
|
878
|
+
* @param {Record<string, unknown>} node AST node
|
|
879
|
+
* @returns {'listen_sql' | 'notify_sql' | 'unlisten_sql' | null} kind знахідки
|
|
880
|
+
*/
|
|
881
|
+
function listenNotifyFromTaggedTemplate(node) {
|
|
882
|
+
if (!node || node.type !== 'TaggedTemplateExpression') return null
|
|
883
|
+
const quasi = node.quasi
|
|
884
|
+
if (!quasi || quasi.type !== 'TemplateLiteral') return null
|
|
885
|
+
const text = templateQuasisText(quasi)
|
|
886
|
+
return kindFromListenNotifyMatch(text)
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Перший аргумент виклику `.query(...)` — це string literal або template literal,
|
|
891
|
+
* текст якого починається з LISTEN / UNLISTEN / NOTIFY (case-insensitive)?
|
|
892
|
+
* @param {unknown} arg AST node першого аргумента
|
|
893
|
+
* @returns {'listen_sql' | 'notify_sql' | 'unlisten_sql' | null} kind знахідки або null
|
|
894
|
+
*/
|
|
895
|
+
function sqlStringStartsWithListenNotify(arg) {
|
|
896
|
+
if (!arg || typeof arg !== 'object') return null
|
|
897
|
+
if ((arg.type === 'Literal' || arg.type === 'StringLiteral') && typeof arg.value === 'string') {
|
|
898
|
+
return kindFromListenNotifyMatch(arg.value)
|
|
899
|
+
}
|
|
900
|
+
if (arg.type === 'TemplateLiteral') {
|
|
901
|
+
return kindFromListenNotifyMatch(templateQuasisText(arg))
|
|
902
|
+
}
|
|
903
|
+
return null
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Перетворює текст SQL-рядка у kind знахідки (`listen_sql` / `notify_sql` /
|
|
908
|
+
* `unlisten_sql`) або `null`, якщо рядок не починається з ключового слова.
|
|
909
|
+
* @param {string} text SQL-текст
|
|
910
|
+
* @returns {'listen_sql' | 'notify_sql' | 'unlisten_sql' | null} kind знахідки
|
|
911
|
+
*/
|
|
912
|
+
function kindFromListenNotifyMatch(text) {
|
|
913
|
+
const m = PG_LISTEN_NOTIFY_SQL_RE.exec(text)
|
|
914
|
+
if (!m) return null
|
|
915
|
+
const keyword = m[1].toUpperCase()
|
|
916
|
+
if (keyword === 'LISTEN') return 'listen_sql'
|
|
917
|
+
if (keyword === 'NOTIFY') return 'notify_sql'
|
|
918
|
+
return 'unlisten_sql'
|
|
919
|
+
}
|
|
920
|
+
|
|
688
921
|
/**
|
|
689
922
|
* Чи сканувати цей файл за розширенням (JS/TS-сімʼя, без `.d.ts`).
|
|
690
923
|
* @param {string} relativePathPosix відносний шлях (posix)
|