@profullstack/threatcrush 0.11.2 → 0.11.3
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/dist/daemon.js +192 -25
- package/dist/daemon.js.map +1 -1
- package/dist/index.js +192 -25
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9939,7 +9939,11 @@ var NODE_RULES = [
|
|
|
9939
9939
|
severity: "medium",
|
|
9940
9940
|
languages: ["javascript", "typescript"],
|
|
9941
9941
|
pattern: /\bBuffer\s*\.\s*allocUnsafe(?:Slow)?\s*\(|\bnew\s+Buffer\s*\(\s*(?![`'"])[a-zA-Z_$0-9]/,
|
|
9942
|
-
inherent: true
|
|
9942
|
+
inherent: true,
|
|
9943
|
+
// Filling the buffer yourself is the whole reason to call `allocUnsafe`,
|
|
9944
|
+
// so reporting every call reports correct code. What is left reported is
|
|
9945
|
+
// an allocation whose bytes are never written before it escapes.
|
|
9946
|
+
filledBeforeUseGuard: true
|
|
9943
9947
|
},
|
|
9944
9948
|
{
|
|
9945
9949
|
id: "js-oversized-request-body-limit",
|
|
@@ -10049,8 +10053,19 @@ var CODE_RULES = [
|
|
|
10049
10053
|
// The tail is what distinguishes assembly from parameterisation. A bound
|
|
10050
10054
|
// query leaves a comma after the closing quote (`"… = $1", [id]`) and
|
|
10051
10055
|
// matches none of these.
|
|
10056
|
+
//
|
|
10057
|
+
// The clause alternative anchors the keyword to the *start* of the
|
|
10058
|
+
// concatenated fragment, because that is where a clause being appended
|
|
10059
|
+
// actually sits: `sql + "WHERE id = " + id`, `sql + " ORDER BY " + col`.
|
|
10060
|
+
//
|
|
10061
|
+
// Allowing it anywhere in the fragment made the rule read English. `WHERE`
|
|
10062
|
+
// and `SET` are ordinary words, and without a leading `\b` they were not
|
|
10063
|
+
// even required to be whole ones — "any`where`" and "sub`set`" both
|
|
10064
|
+
// matched. A help string reading "lists them anywhere" was reported as
|
|
10065
|
+
// critical SQL injection, which is the kind of finding that teaches a team
|
|
10066
|
+
// the scanner is not worth reading.
|
|
10052
10067
|
pattern: new RegExp(
|
|
10053
|
-
`${SQL_STRING}\\s*\\+|${SQL_STRING}\\s*%\\s*[\\w(]|${SQL_STRING}\\s*\\.\\s*format\\s*\\(|\\+\\s*(?:"
|
|
10068
|
+
`${SQL_STRING}\\s*\\+|${SQL_STRING}\\s*%\\s*[\\w(]|${SQL_STRING}\\s*\\.\\s*format\\s*\\(|\\+\\s*(?:"|')\\s*\\b(?:WHERE|ORDER\\s+BY|VALUES|SET)\\b`,
|
|
10054
10069
|
"i"
|
|
10055
10070
|
)
|
|
10056
10071
|
},
|
|
@@ -10063,7 +10078,26 @@ var CODE_RULES = [
|
|
|
10063
10078
|
pattern: new RegExp(
|
|
10064
10079
|
`\`[^\`\\n]*(?:${SQL_KEYWORDS})\\b[^\`\\n]*\\$\\{|\\bf"[^"\\n]*(?:${SQL_KEYWORDS})\\b[^"\\n]*\\{|\\bf'[^'\\n]*(?:${SQL_KEYWORDS})\\b[^'\\n]*\\{`,
|
|
10065
10080
|
"i"
|
|
10066
|
-
)
|
|
10081
|
+
),
|
|
10082
|
+
/**
|
|
10083
|
+
* Interpolating a column list is not interpolating a value.
|
|
10084
|
+
*
|
|
10085
|
+
* const COLS = `tld, user_id, owner_email, price_usd`;
|
|
10086
|
+
* get(`SELECT ${COLS} FROM moshpit_tlds WHERE tld = ?`, [tld]);
|
|
10087
|
+
*
|
|
10088
|
+
* That query *is* parameterised: every value the caller supplies rides a
|
|
10089
|
+
* `?`, and the only thing spliced into the text is a constant written a
|
|
10090
|
+
* few lines up. Naming the column list once instead of repeating it in
|
|
10091
|
+
* fourteen queries is ordinary hygiene, and it is the shape this rule met
|
|
10092
|
+
* most often in practice — one repository produced eighteen findings this
|
|
10093
|
+
* way and not one of them could be injected into.
|
|
10094
|
+
*
|
|
10095
|
+
* The guard resolves the interpolations rather than trusting the shape, so
|
|
10096
|
+
* the moment a query mixes a constant with anything else —
|
|
10097
|
+
* `` `SELECT ${COLS} FROM t WHERE id = ${req.query.id}` `` — it is reported
|
|
10098
|
+
* again. `interpolationsAreConstant` requires *every* `${…}` to resolve.
|
|
10099
|
+
*/
|
|
10100
|
+
constantInterpolationGuard: true
|
|
10067
10101
|
},
|
|
10068
10102
|
{
|
|
10069
10103
|
id: "sql-format-call",
|
|
@@ -10113,7 +10147,22 @@ var CODE_RULES = [
|
|
|
10113
10147
|
cwe: "CWE-78",
|
|
10114
10148
|
severity: "critical",
|
|
10115
10149
|
languages: ["go"],
|
|
10116
|
-
pattern: /\bexec\.Command(?:Context)?\s*\(\s*(?:ctx\s*,\s*)?"(?:\/bin\/)?(?:sh|bash|zsh|cmd|powershell)"\s*,\s*"(?:-c|\/c)"
|
|
10150
|
+
pattern: /\bexec\.Command(?:Context)?\s*\(\s*(?:ctx\s*,\s*)?"(?:\/bin\/)?(?:sh|bash|zsh|cmd|powershell)"\s*,\s*"(?:-c|\/c)"/,
|
|
10151
|
+
// A call whose whole argv is string literals cannot be injected into.
|
|
10152
|
+
// `exec.Command("cmd", "/c", "ver")` reads the Windows version; there is no
|
|
10153
|
+
// value in it for an attacker to reach, and the consequence above — that a
|
|
10154
|
+
// shell will interpret metacharacters — describes metacharacters nobody can
|
|
10155
|
+
// supply. gosec's G204 draws the line in the same place, and this was the
|
|
10156
|
+
// single finding a Go project got out of a whole scan before declining the
|
|
10157
|
+
// offer, which is an expensive way to report nothing.
|
|
10158
|
+
//
|
|
10159
|
+
// The guard has to end at the closing paren, so a literal followed by
|
|
10160
|
+
// anything else still reports: `"ls " + dir` leaves a `+` before the `)`,
|
|
10161
|
+
// `fmt.Sprintf(…)` leaves an identifier, and a bare variable leaves a name.
|
|
10162
|
+
// `(?:[^"\\]|\\.)*` rather than `[^"]*` so an escaped quote inside a
|
|
10163
|
+
// literal — `"echo \"hi\""` — does not end the literal early and drop the
|
|
10164
|
+
// guard on a line it should have covered.
|
|
10165
|
+
lineGuard: /\bexec\.Command(?:Context)?\s*\(\s*(?:ctx\s*,\s*)?"(?:\/bin\/)?(?:sh|bash|zsh|cmd|powershell)"\s*,\s*"(?:-c|\/c)"\s*(?:,\s*"(?:[^"\\]|\\.)*")*\s*,?\s*\)/
|
|
10117
10166
|
},
|
|
10118
10167
|
{
|
|
10119
10168
|
id: "rb-backtick-interpolation",
|
|
@@ -10182,17 +10231,43 @@ var CODE_RULES = [
|
|
|
10182
10231
|
cwe: "CWE-79",
|
|
10183
10232
|
severity: "high",
|
|
10184
10233
|
languages: ["javascript", "typescript"],
|
|
10185
|
-
pattern: /\bdangerouslySetInnerHTML\s*=|\.\s*(?:innerHTML|outerHTML)\s*=\s*(?!\s*['"`]\s*['"`]\s*;?\s*$)|\bdocument\s*\.\s*write(?:ln)?\s*\(|\.\s*insertAdjacentHTML\s*\(/,
|
|
10186
10234
|
/**
|
|
10235
|
+
* The assignment alternative carries its own exemption, as a lookahead, so
|
|
10236
|
+
* that it is decided per assignment rather than per line.
|
|
10237
|
+
*
|
|
10187
10238
|
* A whole-statement assignment of a string with no interpolation and no
|
|
10188
|
-
* concatenation carries no data, so it cannot carry attacker data. This
|
|
10189
|
-
*
|
|
10190
|
-
*
|
|
10191
|
-
*
|
|
10239
|
+
* concatenation carries no data, so it cannot carry attacker data. This was
|
|
10240
|
+
* the single largest source of noise: a codebase that builds its UI with
|
|
10241
|
+
* innerHTML reports every static heading and spinner as XSS, and a rule
|
|
10242
|
+
* that flags 40 safe lines to catch one real one gets switched off.
|
|
10243
|
+
*
|
|
10244
|
+
* Two things this has to get right, and a `lineGuard` could get neither:
|
|
10245
|
+
*
|
|
10246
|
+
* A statement ends at its semicolon, not at the newline. Anchoring to `$`
|
|
10247
|
+
* held the exemption for `el.innerHTML = '';` alone on a line and dropped
|
|
10248
|
+
* it the moment anything followed:
|
|
10249
|
+
*
|
|
10250
|
+
* function hide() { out.hidden = true; out.innerHTML = ''; rendered = null; }
|
|
10192
10251
|
*
|
|
10193
|
-
*
|
|
10252
|
+
* — the same assignment, clearing a node, reported as high-severity XSS
|
|
10253
|
+
* because two neighbours shared its line.
|
|
10254
|
+
*
|
|
10255
|
+
* And an exemption must not become a line-wide amnesty. A guard is tested
|
|
10256
|
+
* against the whole line, so one safe clear would exonerate a real sink
|
|
10257
|
+
* beside it:
|
|
10258
|
+
*
|
|
10259
|
+
* a.innerHTML = ''; b.innerHTML = userInput;
|
|
10260
|
+
*
|
|
10261
|
+
* As a lookahead the regex decides at each `=` it reaches, so the first
|
|
10262
|
+
* assignment is exempt and the second is still reported.
|
|
10263
|
+
*
|
|
10264
|
+
* The whitespace after `=` is matched *inside* the lookahead rather than
|
|
10265
|
+
* before it. Left outside, `\s*` backtracks to zero width, the lookahead
|
|
10266
|
+
* then starts on the space instead of the quote, fails to see a literal,
|
|
10267
|
+
* and the negative lookahead succeeds — reinstating every finding the
|
|
10268
|
+
* exemption was written to remove.
|
|
10194
10269
|
*/
|
|
10195
|
-
|
|
10270
|
+
pattern: /\bdangerouslySetInnerHTML\s*=|\.\s*(?:innerHTML|outerHTML)\s*=(?!\s*(?:'[^'\\\n]*'|"[^"\\\n]*"|`[^`$\\\n]*`)\s*(?:;|$))|\bdocument\s*\.\s*write(?:ln)?\s*\(|\.\s*insertAdjacentHTML\s*\(/
|
|
10196
10271
|
},
|
|
10197
10272
|
{
|
|
10198
10273
|
id: "java-html-writer-concatenation",
|
|
@@ -11130,6 +11205,45 @@ function interpolationsAreConstant(line, fileText) {
|
|
|
11130
11205
|
(expr) => /^[A-Za-z_$][\w$]*$/.test(expr) && isConstantString(expr, fileText)
|
|
11131
11206
|
);
|
|
11132
11207
|
}
|
|
11208
|
+
var FILL_LOOKAHEAD = 16;
|
|
11209
|
+
var NAME = String.raw`(?<![\w$])([A-Za-z_$][\w$]*)`;
|
|
11210
|
+
var ALLOCATION_BINDING = new RegExp(
|
|
11211
|
+
`${NAME}\\s*=\\s*(?:new\\s+Buffer\\s*\\(|Buffer\\s*\\.\\s*allocUnsafe(?:Slow)?\\s*\\()`
|
|
11212
|
+
);
|
|
11213
|
+
function allocationBinding(line) {
|
|
11214
|
+
return ALLOCATION_BINDING.exec(line)?.[1] ?? null;
|
|
11215
|
+
}
|
|
11216
|
+
var WRITE_SHAPES = [
|
|
11217
|
+
// `src.copy(name, …)` — name is the destination.
|
|
11218
|
+
/\.\s*copy\s*\(\s*([A-Za-z_$][\w$]*)\s*[,)]/g,
|
|
11219
|
+
// `name.fill(…)`, `name.write*(…)`, `name.set(…)`.
|
|
11220
|
+
new RegExp(`${NAME}\\s*\\.\\s*(?:fill|set|write[A-Za-z0-9]*)\\s*\\(`, "g"),
|
|
11221
|
+
// `name[i] = …`, but not `name[i] === …`.
|
|
11222
|
+
new RegExp(`${NAME}\\s*\\[[^\\]\\n]*\\]\\s*=(?!=)`, "g")
|
|
11223
|
+
];
|
|
11224
|
+
function writesInto(text, name) {
|
|
11225
|
+
for (const shape of WRITE_SHAPES) {
|
|
11226
|
+
shape.lastIndex = 0;
|
|
11227
|
+
for (let found = shape.exec(text); found !== null; found = shape.exec(text)) {
|
|
11228
|
+
if (found[1] === name) return true;
|
|
11229
|
+
}
|
|
11230
|
+
}
|
|
11231
|
+
return false;
|
|
11232
|
+
}
|
|
11233
|
+
function bufferFilledBeforeUse(ctx) {
|
|
11234
|
+
const line = ctx.lines[ctx.index] ?? "";
|
|
11235
|
+
if (/\ballocUnsafe(?:Slow)?\s*\([^)\n]*\)\s*\.\s*fill\s*\(/.test(line)) return true;
|
|
11236
|
+
const name = allocationBinding(line);
|
|
11237
|
+
if (!name) return false;
|
|
11238
|
+
if (writesInto(line.slice(line.indexOf("=") + 1), name)) return true;
|
|
11239
|
+
const last = Math.min(ctx.lines.length - 1, ctx.index + FILL_LOOKAHEAD);
|
|
11240
|
+
for (let i = ctx.index + 1; i <= last; i += 1) {
|
|
11241
|
+
const next = ctx.lines[i] ?? "";
|
|
11242
|
+
if (skippable(next, i, ctx.prose)) continue;
|
|
11243
|
+
if (writesInto(next, name)) return true;
|
|
11244
|
+
}
|
|
11245
|
+
return false;
|
|
11246
|
+
}
|
|
11133
11247
|
function evaluateRule(rule, ctx) {
|
|
11134
11248
|
if (rule.languages && !rule.languages.includes(ctx.language)) return null;
|
|
11135
11249
|
const line = ctx.lines[ctx.index] ?? "";
|
|
@@ -11148,6 +11262,7 @@ function evaluateRule(rule, ctx) {
|
|
|
11148
11262
|
if (rule.constantInterpolationGuard && interpolationsAreConstant(line, fileTextOf(ctx.lines))) {
|
|
11149
11263
|
return null;
|
|
11150
11264
|
}
|
|
11265
|
+
if (rule.filledBeforeUseGuard && bufferFilledBeforeUse(ctx)) return null;
|
|
11151
11266
|
const guard = rule.guard === void 0 ? GENERIC_GUARD : rule.guard;
|
|
11152
11267
|
if (guard && (guard.test(line) || guard.test(context))) return null;
|
|
11153
11268
|
const untrusted = untrustedPatternFor(ctx.language);
|
|
@@ -11792,6 +11907,11 @@ var KNOWN_PLACEHOLDERS = [
|
|
|
11792
11907
|
function isKnownPlaceholder(text) {
|
|
11793
11908
|
return KNOWN_PLACEHOLDERS.some((pattern) => pattern.test(text));
|
|
11794
11909
|
}
|
|
11910
|
+
function isPlaceholderAttribute(line, value) {
|
|
11911
|
+
const at = line.lastIndexOf(value);
|
|
11912
|
+
if (at === -1) return false;
|
|
11913
|
+
return /(?:^|\s)(?:aria-)?placeholder\s*=\s*[{("'`]*$/i.test(line.slice(0, at));
|
|
11914
|
+
}
|
|
11795
11915
|
function isVariableReference(value) {
|
|
11796
11916
|
const trimmed = value.trim();
|
|
11797
11917
|
const braced = /^\$\{\s*([A-Za-z_][\w.]*)\s*(?::?[-=+?]([\s\S]*))?\}$/.exec(trimmed);
|
|
@@ -11834,10 +11954,13 @@ function words(text) {
|
|
|
11834
11954
|
function isTestFixtureValue(line, value) {
|
|
11835
11955
|
const valueWords = words(value);
|
|
11836
11956
|
if (valueWords.some((word) => FIXTURE_STEMS.some((stem) => word.startsWith(stem)))) return true;
|
|
11957
|
+
return describesItsOwnKey(line, value);
|
|
11958
|
+
}
|
|
11959
|
+
function describesItsOwnKey(line, value) {
|
|
11837
11960
|
if (value.length > 48) return false;
|
|
11838
11961
|
const valueAt = line.lastIndexOf(value);
|
|
11839
11962
|
const key = valueAt === -1 ? line : line.slice(0, valueAt);
|
|
11840
|
-
const flattened =
|
|
11963
|
+
const flattened = words(value).join("");
|
|
11841
11964
|
return words(key).filter((word) => word.length >= 4 && !KEY_NOISE.has(word)).some((word) => flattened.includes(word));
|
|
11842
11965
|
}
|
|
11843
11966
|
function redactSecret(line) {
|
|
@@ -12017,9 +12140,9 @@ function languageOfShebang(firstLine) {
|
|
|
12017
12140
|
}
|
|
12018
12141
|
var SUPPRESS_NEXT = /threatcrush-disable-next-line(?:\s+([\w-]+))?/;
|
|
12019
12142
|
var SUPPRESS_LINE = /threatcrush-disable-line(?:\s+([\w-]+))?/;
|
|
12020
|
-
var
|
|
12021
|
-
function
|
|
12022
|
-
return
|
|
12143
|
+
var FOREIGN_SECURITY = /\b(?:nolint:[\w,]*gosec|nosec)\b/;
|
|
12144
|
+
function foreignSecurityMark(line) {
|
|
12145
|
+
return FOREIGN_SECURITY.test(line);
|
|
12023
12146
|
}
|
|
12024
12147
|
function collectSuppressions(lines) {
|
|
12025
12148
|
const byLine = /* @__PURE__ */ new Map();
|
|
@@ -12045,13 +12168,42 @@ function isSuppressed(suppressions, index, ruleId) {
|
|
|
12045
12168
|
}
|
|
12046
12169
|
function isTestPath(relativePath) {
|
|
12047
12170
|
const p = relativePath.replace(/\\/g, "/");
|
|
12048
|
-
return /(?:^|\/)(?:tests?|__tests__|__mocks__|spec|specs|fixtures?|mocks?|e2e|testdata)\//i.test(p) || /(?:^|\/)(?:test|conftest)_[^/]+$/i.test(p) || /[._-](?:test|spec)\.[a-z]+$/i.test(p) || /_test\.[a-z]+$/i.test(p);
|
|
12171
|
+
return /(?:^|\/)(?:tests?|__tests__|__mocks__|spec|specs|fixtures?|mocks?|e2e|testdata|testutils?|harness)\//i.test(p) || /(?:^|\/)(?:test|conftest)_[^/]+$/i.test(p) || /[._-](?:test|spec)\.[a-z]+$/i.test(p) || /_test\.[a-z]+$/i.test(p);
|
|
12049
12172
|
}
|
|
12173
|
+
function isDocPath(relativePath) {
|
|
12174
|
+
const p = relativePath.replace(/\\/g, "/");
|
|
12175
|
+
return /(?:^|\/)(?:docs?|examples?|samples?)\//i.test(p) || /\.(?:md|mdx|markdown|rst|adoc)$/i.test(p);
|
|
12176
|
+
}
|
|
12177
|
+
var SOFTENING_ORDER = [
|
|
12178
|
+
"suppressed",
|
|
12179
|
+
"placeholder",
|
|
12180
|
+
"self-describing",
|
|
12181
|
+
"test",
|
|
12182
|
+
"docs"
|
|
12183
|
+
];
|
|
12184
|
+
function softening(reasons) {
|
|
12185
|
+
return SOFTENING_ORDER.find((reason) => reasons[reason]) ?? null;
|
|
12186
|
+
}
|
|
12187
|
+
var SECRET_SOFTENING = {
|
|
12188
|
+
test: "in a test file \u2014 usually a fixture, still worth confirming it is not a live credential",
|
|
12189
|
+
docs: "in documentation \u2014 usually an illustrative example, still worth confirming it is not a live credential",
|
|
12190
|
+
suppressed: "on a line already marked as a false positive for another linter's security rule",
|
|
12191
|
+
placeholder: "in example text an empty input field shows, not in data",
|
|
12192
|
+
"self-describing": "in a value that repeats the name of the field holding it \u2014 usually a description of a credential rather than one"
|
|
12193
|
+
};
|
|
12194
|
+
var CODE_SOFTENING = {
|
|
12195
|
+
test: "in a test file, where the construct is ordinary",
|
|
12196
|
+
docs: "in documentation or example code, which nothing runs",
|
|
12197
|
+
suppressed: "on a line another linter's security suppression already covers",
|
|
12198
|
+
placeholder: "in example text rather than in data",
|
|
12199
|
+
"self-describing": "in a value that describes itself"
|
|
12200
|
+
};
|
|
12050
12201
|
function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
12051
12202
|
const findings = [];
|
|
12052
12203
|
const lines = text.split("\n");
|
|
12053
12204
|
const suppressions = collectSuppressions(lines);
|
|
12054
12205
|
const inTests = isTestPath(relativePath);
|
|
12206
|
+
const inDocs = isDocPath(relativePath);
|
|
12055
12207
|
lines.forEach((line, index) => {
|
|
12056
12208
|
for (const rule of SECRET_RULES) {
|
|
12057
12209
|
const match = rule.pattern.exec(line);
|
|
@@ -12061,18 +12213,23 @@ function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
|
12061
12213
|
const value = match[1] ?? match[0];
|
|
12062
12214
|
if (isVariableReference(value)) continue;
|
|
12063
12215
|
if (inTests && rule.keywordShaped && isTestFixtureValue(line, value)) continue;
|
|
12064
|
-
const
|
|
12216
|
+
const soft = softening({
|
|
12217
|
+
test: inTests,
|
|
12218
|
+
suppressed: foreignSecurityMark(line),
|
|
12219
|
+
placeholder: isPlaceholderAttribute(line, value),
|
|
12220
|
+
"self-describing": rule.keywordShaped === true && describesItsOwnKey(line, value)
|
|
12221
|
+
});
|
|
12065
12222
|
findings.push({
|
|
12066
12223
|
ruleId: rule.id,
|
|
12067
12224
|
title: rule.name,
|
|
12068
12225
|
file: relativePath,
|
|
12069
12226
|
line: index + 1,
|
|
12070
|
-
// Reported but not blocking
|
|
12071
|
-
//
|
|
12072
|
-
severity:
|
|
12227
|
+
// Reported but not blocking wherever context weakens the claim — see
|
|
12228
|
+
// `softening`. Never dropped: the count is the same either way.
|
|
12229
|
+
severity: soft ? "low" : rule.severity,
|
|
12073
12230
|
// A matched credential format is the finding, not a proxy for one.
|
|
12074
12231
|
confidence: "evidence",
|
|
12075
|
-
message:
|
|
12232
|
+
message: soft ? `Possible ${rule.name} detected ${SECRET_SOFTENING[soft]}` : `Possible ${rule.name} detected`,
|
|
12076
12233
|
consequence: rule.consequence,
|
|
12077
12234
|
cwe: rule.cwe,
|
|
12078
12235
|
excerpt: redactSecret(line.trim()).slice(0, 200),
|
|
@@ -12087,14 +12244,19 @@ function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
|
12087
12244
|
if (isSuppressed(suppressions, index, rule.id)) continue;
|
|
12088
12245
|
const match = evaluateRule(rule, { lines, index, language, prose });
|
|
12089
12246
|
if (!match) continue;
|
|
12247
|
+
const soft = softening({
|
|
12248
|
+
test: inTests,
|
|
12249
|
+
docs: inDocs,
|
|
12250
|
+
suppressed: foreignSecurityMark(lines[index] ?? "")
|
|
12251
|
+
});
|
|
12090
12252
|
findings.push({
|
|
12091
12253
|
ruleId: rule.id,
|
|
12092
12254
|
title: rule.title,
|
|
12093
12255
|
file: relativePath,
|
|
12094
12256
|
line: index + 1,
|
|
12095
|
-
severity: match.severity,
|
|
12257
|
+
severity: soft ? "low" : match.severity,
|
|
12096
12258
|
confidence: match.confidence,
|
|
12097
|
-
message: `${rule.title} (${rule.cwe})`,
|
|
12259
|
+
message: soft ? `${rule.title} (${rule.cwe}) \u2014 ${CODE_SOFTENING[soft]}` : `${rule.title} (${rule.cwe})`,
|
|
12098
12260
|
consequence: rule.consequence,
|
|
12099
12261
|
cwe: rule.cwe,
|
|
12100
12262
|
excerpt: (lines[index] ?? "").trim().slice(0, 200),
|
|
@@ -12105,14 +12267,19 @@ function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
|
12105
12267
|
for (const match of evaluateTemplateRules(extensionOf(relativePath), lines)) {
|
|
12106
12268
|
const index = match.line - 1;
|
|
12107
12269
|
if (isSuppressed(suppressions, index, match.rule.id)) continue;
|
|
12270
|
+
const soft = softening({
|
|
12271
|
+
test: inTests,
|
|
12272
|
+
docs: inDocs,
|
|
12273
|
+
suppressed: foreignSecurityMark(lines[index] ?? "")
|
|
12274
|
+
});
|
|
12108
12275
|
findings.push({
|
|
12109
12276
|
ruleId: match.rule.id,
|
|
12110
12277
|
title: match.rule.title,
|
|
12111
12278
|
file: relativePath,
|
|
12112
12279
|
line: match.line,
|
|
12113
|
-
severity: match.severity,
|
|
12280
|
+
severity: soft ? "low" : match.severity,
|
|
12114
12281
|
confidence: "pattern",
|
|
12115
|
-
message: `${match.rule.title} (${match.rule.cwe})`,
|
|
12282
|
+
message: soft ? `${match.rule.title} (${match.rule.cwe}) \u2014 ${CODE_SOFTENING[soft]}` : `${match.rule.title} (${match.rule.cwe})`,
|
|
12116
12283
|
consequence: match.rule.consequence,
|
|
12117
12284
|
cwe: match.rule.cwe,
|
|
12118
12285
|
excerpt: (lines[index] ?? "").trim().slice(0, 200),
|