@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/daemon.js
CHANGED
|
@@ -4789,7 +4789,11 @@ var NODE_RULES = [
|
|
|
4789
4789
|
severity: "medium",
|
|
4790
4790
|
languages: ["javascript", "typescript"],
|
|
4791
4791
|
pattern: /\bBuffer\s*\.\s*allocUnsafe(?:Slow)?\s*\(|\bnew\s+Buffer\s*\(\s*(?![`'"])[a-zA-Z_$0-9]/,
|
|
4792
|
-
inherent: true
|
|
4792
|
+
inherent: true,
|
|
4793
|
+
// Filling the buffer yourself is the whole reason to call `allocUnsafe`,
|
|
4794
|
+
// so reporting every call reports correct code. What is left reported is
|
|
4795
|
+
// an allocation whose bytes are never written before it escapes.
|
|
4796
|
+
filledBeforeUseGuard: true
|
|
4793
4797
|
},
|
|
4794
4798
|
{
|
|
4795
4799
|
id: "js-oversized-request-body-limit",
|
|
@@ -4899,8 +4903,19 @@ var CODE_RULES = [
|
|
|
4899
4903
|
// The tail is what distinguishes assembly from parameterisation. A bound
|
|
4900
4904
|
// query leaves a comma after the closing quote (`"… = $1", [id]`) and
|
|
4901
4905
|
// matches none of these.
|
|
4906
|
+
//
|
|
4907
|
+
// The clause alternative anchors the keyword to the *start* of the
|
|
4908
|
+
// concatenated fragment, because that is where a clause being appended
|
|
4909
|
+
// actually sits: `sql + "WHERE id = " + id`, `sql + " ORDER BY " + col`.
|
|
4910
|
+
//
|
|
4911
|
+
// Allowing it anywhere in the fragment made the rule read English. `WHERE`
|
|
4912
|
+
// and `SET` are ordinary words, and without a leading `\b` they were not
|
|
4913
|
+
// even required to be whole ones — "any`where`" and "sub`set`" both
|
|
4914
|
+
// matched. A help string reading "lists them anywhere" was reported as
|
|
4915
|
+
// critical SQL injection, which is the kind of finding that teaches a team
|
|
4916
|
+
// the scanner is not worth reading.
|
|
4902
4917
|
pattern: new RegExp(
|
|
4903
|
-
`${SQL_STRING}\\s*\\+|${SQL_STRING}\\s*%\\s*[\\w(]|${SQL_STRING}\\s*\\.\\s*format\\s*\\(|\\+\\s*(?:"
|
|
4918
|
+
`${SQL_STRING}\\s*\\+|${SQL_STRING}\\s*%\\s*[\\w(]|${SQL_STRING}\\s*\\.\\s*format\\s*\\(|\\+\\s*(?:"|')\\s*\\b(?:WHERE|ORDER\\s+BY|VALUES|SET)\\b`,
|
|
4904
4919
|
"i"
|
|
4905
4920
|
)
|
|
4906
4921
|
},
|
|
@@ -4913,7 +4928,26 @@ var CODE_RULES = [
|
|
|
4913
4928
|
pattern: new RegExp(
|
|
4914
4929
|
`\`[^\`\\n]*(?:${SQL_KEYWORDS})\\b[^\`\\n]*\\$\\{|\\bf"[^"\\n]*(?:${SQL_KEYWORDS})\\b[^"\\n]*\\{|\\bf'[^'\\n]*(?:${SQL_KEYWORDS})\\b[^'\\n]*\\{`,
|
|
4915
4930
|
"i"
|
|
4916
|
-
)
|
|
4931
|
+
),
|
|
4932
|
+
/**
|
|
4933
|
+
* Interpolating a column list is not interpolating a value.
|
|
4934
|
+
*
|
|
4935
|
+
* const COLS = `tld, user_id, owner_email, price_usd`;
|
|
4936
|
+
* get(`SELECT ${COLS} FROM moshpit_tlds WHERE tld = ?`, [tld]);
|
|
4937
|
+
*
|
|
4938
|
+
* That query *is* parameterised: every value the caller supplies rides a
|
|
4939
|
+
* `?`, and the only thing spliced into the text is a constant written a
|
|
4940
|
+
* few lines up. Naming the column list once instead of repeating it in
|
|
4941
|
+
* fourteen queries is ordinary hygiene, and it is the shape this rule met
|
|
4942
|
+
* most often in practice — one repository produced eighteen findings this
|
|
4943
|
+
* way and not one of them could be injected into.
|
|
4944
|
+
*
|
|
4945
|
+
* The guard resolves the interpolations rather than trusting the shape, so
|
|
4946
|
+
* the moment a query mixes a constant with anything else —
|
|
4947
|
+
* `` `SELECT ${COLS} FROM t WHERE id = ${req.query.id}` `` — it is reported
|
|
4948
|
+
* again. `interpolationsAreConstant` requires *every* `${…}` to resolve.
|
|
4949
|
+
*/
|
|
4950
|
+
constantInterpolationGuard: true
|
|
4917
4951
|
},
|
|
4918
4952
|
{
|
|
4919
4953
|
id: "sql-format-call",
|
|
@@ -4963,7 +4997,22 @@ var CODE_RULES = [
|
|
|
4963
4997
|
cwe: "CWE-78",
|
|
4964
4998
|
severity: "critical",
|
|
4965
4999
|
languages: ["go"],
|
|
4966
|
-
pattern: /\bexec\.Command(?:Context)?\s*\(\s*(?:ctx\s*,\s*)?"(?:\/bin\/)?(?:sh|bash|zsh|cmd|powershell)"\s*,\s*"(?:-c|\/c)"
|
|
5000
|
+
pattern: /\bexec\.Command(?:Context)?\s*\(\s*(?:ctx\s*,\s*)?"(?:\/bin\/)?(?:sh|bash|zsh|cmd|powershell)"\s*,\s*"(?:-c|\/c)"/,
|
|
5001
|
+
// A call whose whole argv is string literals cannot be injected into.
|
|
5002
|
+
// `exec.Command("cmd", "/c", "ver")` reads the Windows version; there is no
|
|
5003
|
+
// value in it for an attacker to reach, and the consequence above — that a
|
|
5004
|
+
// shell will interpret metacharacters — describes metacharacters nobody can
|
|
5005
|
+
// supply. gosec's G204 draws the line in the same place, and this was the
|
|
5006
|
+
// single finding a Go project got out of a whole scan before declining the
|
|
5007
|
+
// offer, which is an expensive way to report nothing.
|
|
5008
|
+
//
|
|
5009
|
+
// The guard has to end at the closing paren, so a literal followed by
|
|
5010
|
+
// anything else still reports: `"ls " + dir` leaves a `+` before the `)`,
|
|
5011
|
+
// `fmt.Sprintf(…)` leaves an identifier, and a bare variable leaves a name.
|
|
5012
|
+
// `(?:[^"\\]|\\.)*` rather than `[^"]*` so an escaped quote inside a
|
|
5013
|
+
// literal — `"echo \"hi\""` — does not end the literal early and drop the
|
|
5014
|
+
// guard on a line it should have covered.
|
|
5015
|
+
lineGuard: /\bexec\.Command(?:Context)?\s*\(\s*(?:ctx\s*,\s*)?"(?:\/bin\/)?(?:sh|bash|zsh|cmd|powershell)"\s*,\s*"(?:-c|\/c)"\s*(?:,\s*"(?:[^"\\]|\\.)*")*\s*,?\s*\)/
|
|
4967
5016
|
},
|
|
4968
5017
|
{
|
|
4969
5018
|
id: "rb-backtick-interpolation",
|
|
@@ -5032,17 +5081,43 @@ var CODE_RULES = [
|
|
|
5032
5081
|
cwe: "CWE-79",
|
|
5033
5082
|
severity: "high",
|
|
5034
5083
|
languages: ["javascript", "typescript"],
|
|
5035
|
-
pattern: /\bdangerouslySetInnerHTML\s*=|\.\s*(?:innerHTML|outerHTML)\s*=\s*(?!\s*['"`]\s*['"`]\s*;?\s*$)|\bdocument\s*\.\s*write(?:ln)?\s*\(|\.\s*insertAdjacentHTML\s*\(/,
|
|
5036
5084
|
/**
|
|
5085
|
+
* The assignment alternative carries its own exemption, as a lookahead, so
|
|
5086
|
+
* that it is decided per assignment rather than per line.
|
|
5087
|
+
*
|
|
5037
5088
|
* A whole-statement assignment of a string with no interpolation and no
|
|
5038
|
-
* concatenation carries no data, so it cannot carry attacker data. This
|
|
5039
|
-
*
|
|
5040
|
-
*
|
|
5041
|
-
*
|
|
5089
|
+
* concatenation carries no data, so it cannot carry attacker data. This was
|
|
5090
|
+
* the single largest source of noise: a codebase that builds its UI with
|
|
5091
|
+
* innerHTML reports every static heading and spinner as XSS, and a rule
|
|
5092
|
+
* that flags 40 safe lines to catch one real one gets switched off.
|
|
5093
|
+
*
|
|
5094
|
+
* Two things this has to get right, and a `lineGuard` could get neither:
|
|
5095
|
+
*
|
|
5096
|
+
* A statement ends at its semicolon, not at the newline. Anchoring to `$`
|
|
5097
|
+
* held the exemption for `el.innerHTML = '';` alone on a line and dropped
|
|
5098
|
+
* it the moment anything followed:
|
|
5099
|
+
*
|
|
5100
|
+
* function hide() { out.hidden = true; out.innerHTML = ''; rendered = null; }
|
|
5101
|
+
*
|
|
5102
|
+
* — the same assignment, clearing a node, reported as high-severity XSS
|
|
5103
|
+
* because two neighbours shared its line.
|
|
5104
|
+
*
|
|
5105
|
+
* And an exemption must not become a line-wide amnesty. A guard is tested
|
|
5106
|
+
* against the whole line, so one safe clear would exonerate a real sink
|
|
5107
|
+
* beside it:
|
|
5108
|
+
*
|
|
5109
|
+
* a.innerHTML = ''; b.innerHTML = userInput;
|
|
5042
5110
|
*
|
|
5043
|
-
*
|
|
5111
|
+
* As a lookahead the regex decides at each `=` it reaches, so the first
|
|
5112
|
+
* assignment is exempt and the second is still reported.
|
|
5113
|
+
*
|
|
5114
|
+
* The whitespace after `=` is matched *inside* the lookahead rather than
|
|
5115
|
+
* before it. Left outside, `\s*` backtracks to zero width, the lookahead
|
|
5116
|
+
* then starts on the space instead of the quote, fails to see a literal,
|
|
5117
|
+
* and the negative lookahead succeeds — reinstating every finding the
|
|
5118
|
+
* exemption was written to remove.
|
|
5044
5119
|
*/
|
|
5045
|
-
|
|
5120
|
+
pattern: /\bdangerouslySetInnerHTML\s*=|\.\s*(?:innerHTML|outerHTML)\s*=(?!\s*(?:'[^'\\\n]*'|"[^"\\\n]*"|`[^`$\\\n]*`)\s*(?:;|$))|\bdocument\s*\.\s*write(?:ln)?\s*\(|\.\s*insertAdjacentHTML\s*\(/
|
|
5046
5121
|
},
|
|
5047
5122
|
{
|
|
5048
5123
|
id: "java-html-writer-concatenation",
|
|
@@ -5980,6 +6055,45 @@ function interpolationsAreConstant(line, fileText) {
|
|
|
5980
6055
|
(expr) => /^[A-Za-z_$][\w$]*$/.test(expr) && isConstantString(expr, fileText)
|
|
5981
6056
|
);
|
|
5982
6057
|
}
|
|
6058
|
+
var FILL_LOOKAHEAD = 16;
|
|
6059
|
+
var NAME = String.raw`(?<![\w$])([A-Za-z_$][\w$]*)`;
|
|
6060
|
+
var ALLOCATION_BINDING = new RegExp(
|
|
6061
|
+
`${NAME}\\s*=\\s*(?:new\\s+Buffer\\s*\\(|Buffer\\s*\\.\\s*allocUnsafe(?:Slow)?\\s*\\()`
|
|
6062
|
+
);
|
|
6063
|
+
function allocationBinding(line) {
|
|
6064
|
+
return ALLOCATION_BINDING.exec(line)?.[1] ?? null;
|
|
6065
|
+
}
|
|
6066
|
+
var WRITE_SHAPES = [
|
|
6067
|
+
// `src.copy(name, …)` — name is the destination.
|
|
6068
|
+
/\.\s*copy\s*\(\s*([A-Za-z_$][\w$]*)\s*[,)]/g,
|
|
6069
|
+
// `name.fill(…)`, `name.write*(…)`, `name.set(…)`.
|
|
6070
|
+
new RegExp(`${NAME}\\s*\\.\\s*(?:fill|set|write[A-Za-z0-9]*)\\s*\\(`, "g"),
|
|
6071
|
+
// `name[i] = …`, but not `name[i] === …`.
|
|
6072
|
+
new RegExp(`${NAME}\\s*\\[[^\\]\\n]*\\]\\s*=(?!=)`, "g")
|
|
6073
|
+
];
|
|
6074
|
+
function writesInto(text, name) {
|
|
6075
|
+
for (const shape of WRITE_SHAPES) {
|
|
6076
|
+
shape.lastIndex = 0;
|
|
6077
|
+
for (let found = shape.exec(text); found !== null; found = shape.exec(text)) {
|
|
6078
|
+
if (found[1] === name) return true;
|
|
6079
|
+
}
|
|
6080
|
+
}
|
|
6081
|
+
return false;
|
|
6082
|
+
}
|
|
6083
|
+
function bufferFilledBeforeUse(ctx) {
|
|
6084
|
+
const line = ctx.lines[ctx.index] ?? "";
|
|
6085
|
+
if (/\ballocUnsafe(?:Slow)?\s*\([^)\n]*\)\s*\.\s*fill\s*\(/.test(line)) return true;
|
|
6086
|
+
const name = allocationBinding(line);
|
|
6087
|
+
if (!name) return false;
|
|
6088
|
+
if (writesInto(line.slice(line.indexOf("=") + 1), name)) return true;
|
|
6089
|
+
const last = Math.min(ctx.lines.length - 1, ctx.index + FILL_LOOKAHEAD);
|
|
6090
|
+
for (let i = ctx.index + 1; i <= last; i += 1) {
|
|
6091
|
+
const next = ctx.lines[i] ?? "";
|
|
6092
|
+
if (skippable(next, i, ctx.prose)) continue;
|
|
6093
|
+
if (writesInto(next, name)) return true;
|
|
6094
|
+
}
|
|
6095
|
+
return false;
|
|
6096
|
+
}
|
|
5983
6097
|
function evaluateRule(rule, ctx) {
|
|
5984
6098
|
if (rule.languages && !rule.languages.includes(ctx.language)) return null;
|
|
5985
6099
|
const line = ctx.lines[ctx.index] ?? "";
|
|
@@ -5998,6 +6112,7 @@ function evaluateRule(rule, ctx) {
|
|
|
5998
6112
|
if (rule.constantInterpolationGuard && interpolationsAreConstant(line, fileTextOf(ctx.lines))) {
|
|
5999
6113
|
return null;
|
|
6000
6114
|
}
|
|
6115
|
+
if (rule.filledBeforeUseGuard && bufferFilledBeforeUse(ctx)) return null;
|
|
6001
6116
|
const guard = rule.guard === void 0 ? GENERIC_GUARD : rule.guard;
|
|
6002
6117
|
if (guard && (guard.test(line) || guard.test(context))) return null;
|
|
6003
6118
|
const untrusted = untrustedPatternFor(ctx.language);
|
|
@@ -6642,6 +6757,11 @@ var KNOWN_PLACEHOLDERS = [
|
|
|
6642
6757
|
function isKnownPlaceholder(text) {
|
|
6643
6758
|
return KNOWN_PLACEHOLDERS.some((pattern) => pattern.test(text));
|
|
6644
6759
|
}
|
|
6760
|
+
function isPlaceholderAttribute(line, value) {
|
|
6761
|
+
const at = line.lastIndexOf(value);
|
|
6762
|
+
if (at === -1) return false;
|
|
6763
|
+
return /(?:^|\s)(?:aria-)?placeholder\s*=\s*[{("'`]*$/i.test(line.slice(0, at));
|
|
6764
|
+
}
|
|
6645
6765
|
function isVariableReference(value) {
|
|
6646
6766
|
const trimmed = value.trim();
|
|
6647
6767
|
const braced = /^\$\{\s*([A-Za-z_][\w.]*)\s*(?::?[-=+?]([\s\S]*))?\}$/.exec(trimmed);
|
|
@@ -6684,10 +6804,13 @@ function words(text) {
|
|
|
6684
6804
|
function isTestFixtureValue(line, value) {
|
|
6685
6805
|
const valueWords = words(value);
|
|
6686
6806
|
if (valueWords.some((word) => FIXTURE_STEMS.some((stem) => word.startsWith(stem)))) return true;
|
|
6807
|
+
return describesItsOwnKey(line, value);
|
|
6808
|
+
}
|
|
6809
|
+
function describesItsOwnKey(line, value) {
|
|
6687
6810
|
if (value.length > 48) return false;
|
|
6688
6811
|
const valueAt = line.lastIndexOf(value);
|
|
6689
6812
|
const key = valueAt === -1 ? line : line.slice(0, valueAt);
|
|
6690
|
-
const flattened =
|
|
6813
|
+
const flattened = words(value).join("");
|
|
6691
6814
|
return words(key).filter((word) => word.length >= 4 && !KEY_NOISE.has(word)).some((word) => flattened.includes(word));
|
|
6692
6815
|
}
|
|
6693
6816
|
function redactSecret(line) {
|
|
@@ -6867,9 +6990,9 @@ function languageOfShebang(firstLine) {
|
|
|
6867
6990
|
}
|
|
6868
6991
|
var SUPPRESS_NEXT = /threatcrush-disable-next-line(?:\s+([\w-]+))?/;
|
|
6869
6992
|
var SUPPRESS_LINE = /threatcrush-disable-line(?:\s+([\w-]+))?/;
|
|
6870
|
-
var
|
|
6871
|
-
function
|
|
6872
|
-
return
|
|
6993
|
+
var FOREIGN_SECURITY = /\b(?:nolint:[\w,]*gosec|nosec)\b/;
|
|
6994
|
+
function foreignSecurityMark(line) {
|
|
6995
|
+
return FOREIGN_SECURITY.test(line);
|
|
6873
6996
|
}
|
|
6874
6997
|
function collectSuppressions(lines) {
|
|
6875
6998
|
const byLine = /* @__PURE__ */ new Map();
|
|
@@ -6895,13 +7018,42 @@ function isSuppressed(suppressions, index, ruleId) {
|
|
|
6895
7018
|
}
|
|
6896
7019
|
function isTestPath(relativePath) {
|
|
6897
7020
|
const p = relativePath.replace(/\\/g, "/");
|
|
6898
|
-
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);
|
|
7021
|
+
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);
|
|
6899
7022
|
}
|
|
7023
|
+
function isDocPath(relativePath) {
|
|
7024
|
+
const p = relativePath.replace(/\\/g, "/");
|
|
7025
|
+
return /(?:^|\/)(?:docs?|examples?|samples?)\//i.test(p) || /\.(?:md|mdx|markdown|rst|adoc)$/i.test(p);
|
|
7026
|
+
}
|
|
7027
|
+
var SOFTENING_ORDER = [
|
|
7028
|
+
"suppressed",
|
|
7029
|
+
"placeholder",
|
|
7030
|
+
"self-describing",
|
|
7031
|
+
"test",
|
|
7032
|
+
"docs"
|
|
7033
|
+
];
|
|
7034
|
+
function softening(reasons) {
|
|
7035
|
+
return SOFTENING_ORDER.find((reason) => reasons[reason]) ?? null;
|
|
7036
|
+
}
|
|
7037
|
+
var SECRET_SOFTENING = {
|
|
7038
|
+
test: "in a test file \u2014 usually a fixture, still worth confirming it is not a live credential",
|
|
7039
|
+
docs: "in documentation \u2014 usually an illustrative example, still worth confirming it is not a live credential",
|
|
7040
|
+
suppressed: "on a line already marked as a false positive for another linter's security rule",
|
|
7041
|
+
placeholder: "in example text an empty input field shows, not in data",
|
|
7042
|
+
"self-describing": "in a value that repeats the name of the field holding it \u2014 usually a description of a credential rather than one"
|
|
7043
|
+
};
|
|
7044
|
+
var CODE_SOFTENING = {
|
|
7045
|
+
test: "in a test file, where the construct is ordinary",
|
|
7046
|
+
docs: "in documentation or example code, which nothing runs",
|
|
7047
|
+
suppressed: "on a line another linter's security suppression already covers",
|
|
7048
|
+
placeholder: "in example text rather than in data",
|
|
7049
|
+
"self-describing": "in a value that describes itself"
|
|
7050
|
+
};
|
|
6900
7051
|
function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
6901
7052
|
const findings = [];
|
|
6902
7053
|
const lines = text.split("\n");
|
|
6903
7054
|
const suppressions = collectSuppressions(lines);
|
|
6904
7055
|
const inTests = isTestPath(relativePath);
|
|
7056
|
+
const inDocs = isDocPath(relativePath);
|
|
6905
7057
|
lines.forEach((line, index) => {
|
|
6906
7058
|
for (const rule of SECRET_RULES) {
|
|
6907
7059
|
const match = rule.pattern.exec(line);
|
|
@@ -6911,18 +7063,23 @@ function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
|
6911
7063
|
const value = match[1] ?? match[0];
|
|
6912
7064
|
if (isVariableReference(value)) continue;
|
|
6913
7065
|
if (inTests && rule.keywordShaped && isTestFixtureValue(line, value)) continue;
|
|
6914
|
-
const
|
|
7066
|
+
const soft = softening({
|
|
7067
|
+
test: inTests,
|
|
7068
|
+
suppressed: foreignSecurityMark(line),
|
|
7069
|
+
placeholder: isPlaceholderAttribute(line, value),
|
|
7070
|
+
"self-describing": rule.keywordShaped === true && describesItsOwnKey(line, value)
|
|
7071
|
+
});
|
|
6915
7072
|
findings.push({
|
|
6916
7073
|
ruleId: rule.id,
|
|
6917
7074
|
title: rule.name,
|
|
6918
7075
|
file: relativePath,
|
|
6919
7076
|
line: index + 1,
|
|
6920
|
-
// Reported but not blocking
|
|
6921
|
-
//
|
|
6922
|
-
severity:
|
|
7077
|
+
// Reported but not blocking wherever context weakens the claim — see
|
|
7078
|
+
// `softening`. Never dropped: the count is the same either way.
|
|
7079
|
+
severity: soft ? "low" : rule.severity,
|
|
6923
7080
|
// A matched credential format is the finding, not a proxy for one.
|
|
6924
7081
|
confidence: "evidence",
|
|
6925
|
-
message:
|
|
7082
|
+
message: soft ? `Possible ${rule.name} detected ${SECRET_SOFTENING[soft]}` : `Possible ${rule.name} detected`,
|
|
6926
7083
|
consequence: rule.consequence,
|
|
6927
7084
|
cwe: rule.cwe,
|
|
6928
7085
|
excerpt: redactSecret(line.trim()).slice(0, 200),
|
|
@@ -6937,14 +7094,19 @@ function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
|
6937
7094
|
if (isSuppressed(suppressions, index, rule.id)) continue;
|
|
6938
7095
|
const match = evaluateRule(rule, { lines, index, language, prose });
|
|
6939
7096
|
if (!match) continue;
|
|
7097
|
+
const soft = softening({
|
|
7098
|
+
test: inTests,
|
|
7099
|
+
docs: inDocs,
|
|
7100
|
+
suppressed: foreignSecurityMark(lines[index] ?? "")
|
|
7101
|
+
});
|
|
6940
7102
|
findings.push({
|
|
6941
7103
|
ruleId: rule.id,
|
|
6942
7104
|
title: rule.title,
|
|
6943
7105
|
file: relativePath,
|
|
6944
7106
|
line: index + 1,
|
|
6945
|
-
severity: match.severity,
|
|
7107
|
+
severity: soft ? "low" : match.severity,
|
|
6946
7108
|
confidence: match.confidence,
|
|
6947
|
-
message: `${rule.title} (${rule.cwe})`,
|
|
7109
|
+
message: soft ? `${rule.title} (${rule.cwe}) \u2014 ${CODE_SOFTENING[soft]}` : `${rule.title} (${rule.cwe})`,
|
|
6948
7110
|
consequence: rule.consequence,
|
|
6949
7111
|
cwe: rule.cwe,
|
|
6950
7112
|
excerpt: (lines[index] ?? "").trim().slice(0, 200),
|
|
@@ -6955,14 +7117,19 @@ function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
|
6955
7117
|
for (const match of evaluateTemplateRules(extensionOf(relativePath), lines)) {
|
|
6956
7118
|
const index = match.line - 1;
|
|
6957
7119
|
if (isSuppressed(suppressions, index, match.rule.id)) continue;
|
|
7120
|
+
const soft = softening({
|
|
7121
|
+
test: inTests,
|
|
7122
|
+
docs: inDocs,
|
|
7123
|
+
suppressed: foreignSecurityMark(lines[index] ?? "")
|
|
7124
|
+
});
|
|
6958
7125
|
findings.push({
|
|
6959
7126
|
ruleId: match.rule.id,
|
|
6960
7127
|
title: match.rule.title,
|
|
6961
7128
|
file: relativePath,
|
|
6962
7129
|
line: match.line,
|
|
6963
|
-
severity: match.severity,
|
|
7130
|
+
severity: soft ? "low" : match.severity,
|
|
6964
7131
|
confidence: "pattern",
|
|
6965
|
-
message: `${match.rule.title} (${match.rule.cwe})`,
|
|
7132
|
+
message: soft ? `${match.rule.title} (${match.rule.cwe}) \u2014 ${CODE_SOFTENING[soft]}` : `${match.rule.title} (${match.rule.cwe})`,
|
|
6966
7133
|
consequence: match.rule.consequence,
|
|
6967
7134
|
cwe: match.rule.cwe,
|
|
6968
7135
|
excerpt: (lines[index] ?? "").trim().slice(0, 200),
|