@clear-capabilities/agentic-security-scanner 0.195.13 → 0.195.15
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 +32 -0
- package/dist/{1f01fe10027e772f73a1.json → 9699866704e8a4a5839f.json} +2 -2
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +2 -2
- package/src/dataflow/engine.js +19 -4
- package/src/language/CLAUDE.md +3 -0
- package/src/language/haskell-ir.js +88 -14
- package/src/language/haskell-models.js +11 -0
- package/src/language/haskell-parser.js +104 -4
- package/src/language/haskell-security-rules.js +41 -2
- package/src/language/haskell-syntax.js +86 -4
|
@@ -18,6 +18,12 @@ const KW = new Set(['case', 'class', 'data', 'default', 'deriving', 'do', 'else'
|
|
|
18
18
|
const NUMERIC_TYPE = /^\(?\s*(?:(?:Maybe|IO|ActionM|Handler|Either\s+String)\s+)?\(?\s*(?:Int|Integer|Int(?:8|16|32|64)|Word|Word(?:8|16|32|64)|Double|Float|Natural|Bool|Scientific)\s*\)?\s*\)?$/;
|
|
19
19
|
// The module enables Template Haskell (a LANGUAGE pragma; a cabal `default-extensions` entry is not seen here, a stated limit).
|
|
20
20
|
const TH_PRAGMA = /\{-#\s*LANGUAGE\b[^#]*\bTemplateHaskell\b/;
|
|
21
|
+
// OverloadedRecordDot: `r.field` (no spaces, lowercase field) is a field selection, `(.field)` a selector section. OverloadedLabels: `#field`
|
|
22
|
+
// in prefix position is a label value (IHP's `get #name r`, `set #name v r`). Both read from the LANGUAGE pragma only, as above.
|
|
23
|
+
const RECORD_DOT_PRAGMA = /\{-#\s*LANGUAGE\b[^#]*\bOverloadedRecordDot\b/;
|
|
24
|
+
const LABELS_PRAGMA = /\{-#\s*LANGUAGE\b[^#]*\bOverloadedLabels\b/;
|
|
25
|
+
const RECDO_PRAGMA = /\{-#\s*LANGUAGE\b[^#]*\bRecursiveDo\b/;
|
|
26
|
+
const DIGEST_TYPE = /^\(?\s*Digest\s+([A-Za-z][A-Za-z0-9_]*)\s*\)?$/;
|
|
21
27
|
const RESERVED_OPS = new Set(['=', '|', '->', '<-', '::', '=>', '\\', '@', '~', '..']);
|
|
22
28
|
const SYM = new Set('!#$%&*+./<=>?@\\^|-~:'.split(''));
|
|
23
29
|
const OPEN = { '(': ')', '[': ']', '{': '}' };
|
|
@@ -36,6 +42,9 @@ export function lexHs(src, opts = {}) {
|
|
|
36
42
|
const T = [];
|
|
37
43
|
const n = src.length;
|
|
38
44
|
const thSplices = !!opts.th;
|
|
45
|
+
const recordDot = !!opts.recordDot;
|
|
46
|
+
const labels = !!opts.labels;
|
|
47
|
+
const recursiveDo = !!opts.recursiveDo;
|
|
39
48
|
let i = 0;
|
|
40
49
|
let line = 1;
|
|
41
50
|
let col = 0;
|
|
@@ -138,7 +147,9 @@ export function lexHs(src, opts = {}) {
|
|
|
138
147
|
let k = i;
|
|
139
148
|
while (isIdChar(src[k])) k++;
|
|
140
149
|
const v = src.slice(i, k);
|
|
141
|
-
|
|
150
|
+
// `mdo` (RecursiveDo) is a `do` whose statements may refer to later ones; for the lowering that is a `do`. (`rec` blocks are not read.)
|
|
151
|
+
if (v === 'mdo' && recursiveDo) push('kw', 'do', {}, k - i);
|
|
152
|
+
else push(KW.has(v) ? 'kw' : 'var', v, {}, k - i);
|
|
142
153
|
continue;
|
|
143
154
|
}
|
|
144
155
|
if (c === '(' || c === ')' || c === '[' || c === ']' || c === ',' || c === ';' || c === '`' || c === '{' || c === '}') {
|
|
@@ -153,6 +164,22 @@ export function lexHs(src, opts = {}) {
|
|
|
153
164
|
const prefix = !(isIdChar(prev) || isSym(prev) || prev === ')' || prev === ']' || prev === '}' || prev === '"' || prev === "'");
|
|
154
165
|
if (prefix && (src[k] === '(' || (isIdStart(src[k]) && !isUpper(src[k])))) { push('splice', src.slice(i, k), {}, k - i); continue; }
|
|
155
166
|
}
|
|
167
|
+
// `r.field` / `(e).field` / `r.a.b` with OverloadedRecordDot: a `.` touching an identifier, `)` or `}` on its left and a lowercase name on
|
|
168
|
+
// its right is a field selection (`sel` token), never composition (GHC reads it the same way under the extension). `(.field)` is the
|
|
169
|
+
// selector section: a `.` touching `(` on its left.
|
|
170
|
+
if (c === '.' && recordDot && isIdStart(src[i + 1]) && !isUpper(src[i + 1]) && i > 0 && (isIdChar(src[i - 1]) || src[i - 1] === ')' || src[i - 1] === '}' || src[i - 1] === '(')) {
|
|
171
|
+
let m = i + 1;
|
|
172
|
+
while (isIdChar(src[m])) m++;
|
|
173
|
+
push('sel', src.slice(i + 1, m), { section: src[i - 1] === '(' }, m - i);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
// `#field` with OverloadedLabels, in prefix position: a label value.
|
|
177
|
+
if (c === '#' && labels && isIdStart(src[i + 1]) && !isUpper(src[i + 1]) && (i === 0 || !(isIdChar(src[i - 1]) || isSym(src[i - 1]) || src[i - 1] === ')' || src[i - 1] === ']'))) {
|
|
178
|
+
let m = i + 1;
|
|
179
|
+
while (isIdChar(src[m])) m++;
|
|
180
|
+
push('label', src.slice(i + 1, m), {}, m - i);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
156
183
|
if (isSym(c)) {
|
|
157
184
|
let j = i;
|
|
158
185
|
while (isSym(src[j])) j++;
|
|
@@ -196,7 +223,7 @@ const BT_FIX = { elem: [4, 'l'], notElem: [4, 'l'], div: [7, 'l'], mod: [7, 'l']
|
|
|
196
223
|
* @returns {{tokens:object[], module:object|null, imports:object[], decls:object[], sigs:object[], data:object[], classes:object[], instances:object[], errors:object[], boundaries:object[]}}
|
|
197
224
|
*/
|
|
198
225
|
export function parseSyntax(src, opts = {}) {
|
|
199
|
-
const T = lexHs(src, { th: TH_PRAGMA.test(src) });
|
|
226
|
+
const T = lexHs(src, { th: TH_PRAGMA.test(src), recordDot: RECORD_DOT_PRAGMA.test(src), labels: LABELS_PRAGMA.test(src), recursiveDo: RECDO_PRAGMA.test(src) });
|
|
200
227
|
const mate = matchBrackets(T);
|
|
201
228
|
const N = T.length;
|
|
202
229
|
const errors = [];
|
|
@@ -433,6 +460,8 @@ export function parseSyntax(src, opts = {}) {
|
|
|
433
460
|
// lowers to a constant (no caller text survives such a type). Every other annotation is dropped as before.
|
|
434
461
|
const typeText = T.slice(from, pos).map((x) => x.v).join(' ');
|
|
435
462
|
if (NUMERIC_TYPE.test(typeText)) e = { t: 'typed', e, type: typeText, line: e && e.line };
|
|
463
|
+
// `hash x :: Digest MD5`: the annotation chooses the digest algorithm (type-directed selection); kept for the weak-hash rule.
|
|
464
|
+
else { const m = DIGEST_TYPE.exec(typeText); if (m) e = { t: 'typed', e, type: typeText, digest: m[1], line: e && e.line }; }
|
|
436
465
|
}
|
|
437
466
|
return e;
|
|
438
467
|
} finally { depth--; }
|
|
@@ -469,6 +498,19 @@ export function parseSyntax(src, opts = {}) {
|
|
|
469
498
|
pos = blk.b;
|
|
470
499
|
return { t: 'lamcase', alts: withIndent(blk.indent, () => blk.items.map(([a, b]) => parseAlt(a, b))), line: t.line };
|
|
471
500
|
}
|
|
501
|
+
// `\cases` (GHC 9.4): alternatives with SEVERAL patterns each, `0 y -> ..; x y -> ..`.
|
|
502
|
+
if (peek() && peek().t === 'var' && peek().v === 'cases') {
|
|
503
|
+
const blk = blockAfter(pos, lim, layoutIndent);
|
|
504
|
+
pos = blk.b;
|
|
505
|
+
const parseAltMulti = (a, b) => {
|
|
506
|
+
const k = findTop(a, b, (x) => isRop(x, '->') || isRop(x, '|'));
|
|
507
|
+
if (k < 0) { err('expected-arrow', T[a], 'case alternative has no `->`'); return { pats: [{ t: 'pwild' }], rhs: { guards: null, body: { t: 'unknown' }, where: [] }, line: T[a].line, bad: true }; }
|
|
508
|
+
const pats = [];
|
|
509
|
+
for (let q = a; q < k;) { const r = apatAt(q, k); if (!r) break; pats.push(r[0]); q = r[1]; }
|
|
510
|
+
return { pats, rhs: parseRhs(k, b, '->'), line: T[a].line };
|
|
511
|
+
};
|
|
512
|
+
return { t: 'lamcases', alts: withIndent(blk.indent, () => blk.items.map(([a, b]) => parseAltMulti(a, b))), line: t.line };
|
|
513
|
+
}
|
|
472
514
|
const params = [];
|
|
473
515
|
while (pos < lim && !isRop(T[pos], '->')) {
|
|
474
516
|
const r = apatAt(pos, lim);
|
|
@@ -532,7 +574,7 @@ export function parseSyntax(src, opts = {}) {
|
|
|
532
574
|
function startsAexp() {
|
|
533
575
|
const t = peek();
|
|
534
576
|
if (!t) return false;
|
|
535
|
-
if (t.t === 'var' || t.t === 'con' || t.t === 'int' || t.t === 'float' || t.t === 'str' || t.t === 'chr' || t.t === 'splice') return true;
|
|
577
|
+
if (t.t === 'var' || t.t === 'con' || t.t === 'int' || t.t === 'float' || t.t === 'str' || t.t === 'chr' || t.t === 'splice' || t.t === 'label') return true;
|
|
536
578
|
return isSp(t, '(') || isSp(t, '[');
|
|
537
579
|
}
|
|
538
580
|
function fexp() {
|
|
@@ -565,6 +607,7 @@ export function parseSyntax(src, opts = {}) {
|
|
|
565
607
|
else if (peek() && peek().t === 'var') pos++;
|
|
566
608
|
return { t: 'splice', line: t.line };
|
|
567
609
|
}
|
|
610
|
+
else if (t.t === 'label') { pos++; e = { t: 'label', name: t.v, line: t.line }; }
|
|
568
611
|
else { err('unexpected-token', t, `unexpected token ${t.v}`); pos++; return { t: 'unknown', line: t.line }; }
|
|
569
612
|
while (isSp(peek(), '{') && mate[pos] > pos && mate[pos] < lim && (e.t === 'con' || e.t === 'var' || e.t === 'rec' || e.t === 'paren')) {
|
|
570
613
|
const close = mate[pos];
|
|
@@ -580,6 +623,8 @@ export function parseSyntax(src, opts = {}) {
|
|
|
580
623
|
e = e.t === 'con' ? { t: 'rec', con: e, fields, line: t.line } : { t: 'recupd', base: e, fields, line: t.line };
|
|
581
624
|
pos = close + 1;
|
|
582
625
|
}
|
|
626
|
+
// `e.field` (OverloadedRecordDot): the field selector applied to `e`, which the IR lowers to a member read of a known record field.
|
|
627
|
+
while (peek() && peek().t === 'sel' && !peek().section) { const s = peek(); pos++; e = { t: 'app', f: { t: 'var', name: s.v, qual: null, line: s.line }, args: [e], line: s.line, recordDot: true }; }
|
|
583
628
|
return e;
|
|
584
629
|
}
|
|
585
630
|
const isOpTok = (x) => x && x.t === 'op';
|
|
@@ -594,6 +639,8 @@ export function parseSyntax(src, opts = {}) {
|
|
|
594
639
|
}
|
|
595
640
|
const [s, e] = parts[0];
|
|
596
641
|
const first = T[s];
|
|
642
|
+
// `(.field)` (OverloadedRecordDot): the selector function itself.
|
|
643
|
+
if (first && first.t === 'sel' && first.section && e - s === 1) return { t: 'var', name: first.v, qual: null, line };
|
|
597
644
|
if (isOpTok(first) && e - s === 1) return { t: 'var', name: first.v, qual: first.q || null, op: true, line };
|
|
598
645
|
if (isOpTok(first) && first.v !== '-') return { t: 'rsec', op: first.v, qual: first.q || null, e: sub(s + 1, e, expr0), line };
|
|
599
646
|
if (isSp(first, '`') && T[s + 2] && isSp(T[s + 2], '`') && e - s > 3) return { t: 'rsec', op: T[s + 1].v, qual: T[s + 1].q || null, e: sub(s + 3, e, expr0), line, backtick: true };
|
|
@@ -700,6 +747,9 @@ export function parseSyntax(src, opts = {}) {
|
|
|
700
747
|
return out;
|
|
701
748
|
}
|
|
702
749
|
function parseDecl(a, b) {
|
|
750
|
+
// `pattern P :: T` / `pattern P x = ..` (PatternSynonyms): a synonym, recorded by name, not a function called `pattern`. Its right-hand
|
|
751
|
+
// side is not modelled (a match on `P` is a constructor pattern to the IR, which is sound for taint: it binds what it binds).
|
|
752
|
+
if (T[a].t === 'var' && T[a].v === 'pattern' && T[a + 1] && T[a + 1].t === 'con' && a + 1 < b) return { t: 'patsyn', name: T[a + 1].v, line: T[a].line };
|
|
703
753
|
const mark = findTop(a, b, (x) => x.t === 'rop' && (x.v === '=' || x.v === '|' || x.v === '::'));
|
|
704
754
|
if (mark < 0) { err('expected-declaration', T[a], 'item is neither a binding nor a signature'); return null; }
|
|
705
755
|
if (T[mark].v === '::') {
|
|
@@ -754,7 +804,39 @@ export function parseSyntax(src, opts = {}) {
|
|
|
754
804
|
let nameTok = null;
|
|
755
805
|
for (let q = a + 1; q < (eq < 0 ? b : eq); q++) if (T[q].t === 'con') { nameTok = T[q]; break; }
|
|
756
806
|
const decl = { t: 'data', newtype: isNewtype, name: nameTok ? nameTok.v : null, constructors: [], line: T[a].line };
|
|
757
|
-
if (eq < 0)
|
|
807
|
+
if (eq < 0) {
|
|
808
|
+
// GADT syntax: `data T where { A :: Int -> T; B :: { fld :: String } -> T }`. Each item names one or more constructors, their record
|
|
809
|
+
// fields when written with braces, else an arity of one per top-level arrow; the result type is not read (no type checking).
|
|
810
|
+
const w = findTop(a, b, (x) => isKw(x, 'where'));
|
|
811
|
+
if (w < 0) return decl;
|
|
812
|
+
const blk = blockAfter(w, b, layoutIndent);
|
|
813
|
+
for (const [s, e] of blk.items) {
|
|
814
|
+
const names = [];
|
|
815
|
+
let i = s;
|
|
816
|
+
while (i < e && T[i].t === 'con') { names.push(T[i].v); i++; if (isSp(T[i], ',')) i++; else break; }
|
|
817
|
+
if (!names.length || !isRop(T[i], '::')) continue;
|
|
818
|
+
i++;
|
|
819
|
+
while (i < e && ((T[i].t === 'var' && T[i].v === 'forall') || T[i].t === 'op' || isRop(T[i], '=>'))) i = findTop(i, e, (x) => isRop(x, '=>')) >= 0 ? findTop(i, e, (x) => isRop(x, '=>')) + 1 : e;
|
|
820
|
+
let fields = null;
|
|
821
|
+
let arity = 0;
|
|
822
|
+
if (isSp(T[i], '{') && mate[i] > i) {
|
|
823
|
+
fields = [];
|
|
824
|
+
const close = mate[i];
|
|
825
|
+
let j = i + 1;
|
|
826
|
+
while (j < close) {
|
|
827
|
+
const ns = [];
|
|
828
|
+
while (j < close && T[j].t === 'var') { ns.push(T[j].v); j++; if (isSp(T[j], ',')) j++; else break; }
|
|
829
|
+
if (isRop(T[j], '::')) { j++; while (j < close && !isSp(T[j], ',')) j = Math.min(close, groupEnd(j)); j++; } else break;
|
|
830
|
+
fields.push(...ns);
|
|
831
|
+
}
|
|
832
|
+
arity = fields.length;
|
|
833
|
+
} else {
|
|
834
|
+
for (let j = i; j < e;) { if (isRop(T[j], '->')) arity++; j = Math.min(e, groupEnd(j)); }
|
|
835
|
+
}
|
|
836
|
+
for (const n of names) decl.constructors.push({ name: n, fields, arity });
|
|
837
|
+
}
|
|
838
|
+
return decl;
|
|
839
|
+
}
|
|
758
840
|
let end = findTop(eq + 1, b, (x) => isKw(x, 'deriving'));
|
|
759
841
|
if (end < 0) end = b;
|
|
760
842
|
for (const [s0, e] of splitTop(eq + 1, end, (x) => isRop(x, '|'))) {
|