cryptoserve
Advanced tools
+96
-6
@@ -34,6 +34,20 @@ /** | ||
| function ruleIdFor(finding) { | ||
| // One case for one algorithm. The scanner canonicalizes source tokens to | ||
| // lowercase (`md5`), the package database spells the same algorithm as it | ||
| // reads in a datasheet (`MD5`), and a gate reporting the manifest version | ||
| // filed the same defect under a second rule id that no `scan` run ever | ||
| // produced -- so an alert closed and reopened depending on which command had | ||
| // run last. | ||
| const algorithm = String(finding.algorithm || 'unknown').toLowerCase(); | ||
| if (finding.kind === 'secret') return `cryptoserve/secret/${finding.type}`; | ||
| if (finding.kind === 'weak-algorithm') return `cryptoserve/weak-algorithm/${finding.algorithm || 'unknown'}`; | ||
| if (finding.kind === 'weak-algorithm') return `cryptoserve/weak-algorithm/${algorithm}`; | ||
| if (finding.kind === 'misuse') return 'cryptoserve/api-misuse'; | ||
| if (finding.kind === 'tls') return `cryptoserve/tls/${finding.protocol || 'unknown'}`; | ||
| // A committed private key used to fall through to the catch-all id, so it | ||
| // shared one rule with everything else unclassified: code scanning groups by | ||
| // ruleId, and the group took its description from whichever finding arrived | ||
| // first. | ||
| if (finding.kind === 'private-key') return 'cryptoserve/private-key'; | ||
| if (finding.kind === 'quantum-risk') return `cryptoserve/quantum-risk/${algorithm}`; | ||
| if (finding.kind === 'score') return 'cryptoserve/quantum-readiness-score'; | ||
| return 'cryptoserve/finding'; | ||
@@ -118,5 +132,76 @@ } | ||
| /** | ||
| * Normalize the GATE's violations into the same flat finding list. | ||
| * | ||
| * `gate --format sarif` used to call collectFindings(scanResults), which is the | ||
| * SCAN's answer to a different question. The document was therefore | ||
| * byte-identical to `scan --format sarif` whatever the gate had decided: a gate | ||
| * that failed the build on three manifest violations uploaded a report naming | ||
| * none of them, and a gate that passed uploaded alerts for findings it had just | ||
| * accepted. Three formats, one decision, so the SARIF is built from the same | ||
| * `violations` array the text and JSON renderings read. | ||
| * | ||
| * Every violation in the list failed the gate, so each result says which | ||
| * threshold it breached. That sentence is the answer to "why is this build | ||
| * red", and it existed in no format before. | ||
| * | ||
| * @param {Array<object>} violations - the gate's violation list | ||
| * @param {{maxRisk?: string, maxSeverity?: string}} thresholds - what the gate enforced | ||
| * @returns {Array<object>} findings, in the shape toSarif() renders | ||
| */ | ||
| export function violationsToFindings(violations, thresholds = {}) { | ||
| const { maxRisk, maxSeverity } = thresholds; | ||
| return (violations || []).map((v) => { | ||
| const kind = v.type === 'secret' ? 'secret' | ||
| : v.type === 'private-key' ? 'private-key' | ||
| : v.type === 'tls' ? 'tls' | ||
| : v.type === 'misuse' ? 'misuse' | ||
| // An algorithm-level violation raised only by --max-risk is not a | ||
| // statement about how the code is written. Keeping it under its own rule | ||
| // id stops a quantum migration item and a broken hash from sharing an | ||
| // alert group. | ||
| : (v.severity || v.weak) ? 'weak-algorithm' | ||
| : 'quantum-risk'; | ||
| const why = []; | ||
| if (v.severityBreach) why.push(`severity ${v.severity} exceeds --max-severity ${maxSeverity}`); | ||
| if (v.riskBreach) why.push(`quantum risk ${v.risk} exceeds --max-risk ${maxRisk}`); | ||
| if (v.weak && !v.severityBreach && !v.riskBreach) why.push('reported weak under --fail-on-weak'); | ||
| if (kind === 'secret' || kind === 'private-key') { | ||
| why.push('credential findings fail the gate unless waived with --allow-secrets'); | ||
| } | ||
| const headline = kind === 'secret' ? `Hardcoded credential: ${v.algorithm}` : v.algorithm; | ||
| return { | ||
| kind, | ||
| algorithm: v.algorithm, | ||
| // The identity the scanner gave the finding, so one tree gets one rule id | ||
| // whether it was reported by `scan` or by `gate`. | ||
| type: v.secretType, | ||
| protocol: v.protocol, | ||
| title: headline, | ||
| message: why.length > 0 ? `${headline} (${why.join('; ')})` : headline, | ||
| severity: v.severity, | ||
| // Stated rather than derived from `severity`. A violation raised only by | ||
| // --max-risk has no security severity to map -- reporting its quantum | ||
| // risk in a field named severity is the conflation #58 removed -- but it | ||
| // still failed a build and needs a level a reader can rank. | ||
| level: toLevel(v.severity || (v.riskBreach ? v.risk : null)), | ||
| // A dependency named only in a manifest has no source line. The manifest | ||
| // that declares it is a location a reader can open, and a SARIF result | ||
| // with no location at all is dropped by code scanning, so the finding | ||
| // that failed the build would appear nowhere. | ||
| file: v.file || v.manifest, | ||
| line: v.line, | ||
| cwe: v.cwe, | ||
| fix: v.reason, | ||
| }; | ||
| }); | ||
| } | ||
| /** | ||
| * Render findings as a SARIF 2.1.0 document. | ||
| * | ||
| * @param {Array<object>} findings - from collectFindings() | ||
| * @param {Array<object>} findings - from collectFindings() or violationsToFindings() | ||
| * @returns {object} SARIF log object, ready for JSON.stringify | ||
@@ -129,2 +214,7 @@ */ | ||
| const ruleId = ruleIdFor(finding); | ||
| // A rule describes a class of defect; a result describes one instance. The | ||
| // gate's message names the threshold this instance breached, which is true | ||
| // of the result and not of the rule, so the rule keeps the plain title. | ||
| const level = finding.level || toLevel(finding.severity); | ||
| const title = finding.title || finding.message; | ||
| if (!rules.has(ruleId)) { | ||
@@ -136,5 +226,5 @@ const help = [finding.fix ? `Fix: ${finding.fix}` : null, finding.cwe ? `See ${finding.cwe}.` : null] | ||
| name: ruleId.split('/').slice(1).join('-') || 'finding', | ||
| shortDescription: { text: finding.message }, | ||
| fullDescription: { text: help || finding.message }, | ||
| defaultConfiguration: { level: toLevel(finding.severity) }, | ||
| shortDescription: { text: title }, | ||
| fullDescription: { text: help || title }, | ||
| defaultConfiguration: { level }, | ||
| ...(help ? { help: { text: help } } : {}), | ||
@@ -150,3 +240,3 @@ ...(finding.cwe ? { properties: { tags: ['security', 'cryptography', finding.cwe] } } : { | ||
| ruleId, | ||
| level: toLevel(finding.severity), | ||
| level, | ||
| message: { text }, | ||
@@ -153,0 +243,0 @@ locations: physicalLocation(finding), |
+67
-1
@@ -65,2 +65,42 @@ /** | ||
| /** | ||
| * The security severity `scan` reports for a weak algorithm. | ||
| * | ||
| * Exported because `gate` needs the same answer for algorithms it only ever | ||
| * sees named in a dependency manifest, where no source line was read and so no | ||
| * weakPattern exists to carry a severity. Restating the rule in the gate would | ||
| * let the two commands drift into rating the same algorithm differently, which | ||
| * is the class of disagreement issue #58 is about. | ||
| * | ||
| * Returns null for an algorithm that is not weak: absence of a security finding | ||
| * is not the same claim as a finding of severity `none`. | ||
| */ | ||
| export function weakAlgorithmSeverity(dbEntry) { | ||
| if (!dbEntry || !dbEntry.isWeak) return null; | ||
| return dbEntry.quantumRisk === 'critical' ? 'critical' : 'high'; | ||
| } | ||
| /** The severity ladder, lowest to highest. */ | ||
| export const SEVERITY_ORDER = ['none', 'low', 'medium', 'high', 'critical']; | ||
| /** | ||
| * Does a finding's severity exceed a threshold? | ||
| * | ||
| * Unknown severities fail CLOSED. `SEVERITY_ORDER.indexOf` returns -1 for a | ||
| * value the ladder does not know, and `-1 > anything` is false, so comparing | ||
| * indices directly means a typo in one pattern definition -- `severity: | ||
| * 'moderate'` -- makes that finding silently unable to breach any threshold. | ||
| * The gate would report the tree clean and never mention the finding it could | ||
| * not classify. A gate must not be disarmed by a misspelling. | ||
| * | ||
| * A null severity means the scanner reported no security finding at all, which | ||
| * is a different claim from a finding of severity `none`, and never breaches. | ||
| */ | ||
| export function exceedsSeverity(severity, threshold) { | ||
| if (severity === null || severity === undefined) return false; | ||
| const idx = SEVERITY_ORDER.indexOf(severity); | ||
| if (idx === -1) return true; | ||
| return idx > SEVERITY_ORDER.indexOf(threshold); | ||
| } | ||
| // Modes that are not weak on their own but carry a caveat worth surfacing. | ||
@@ -184,2 +224,11 @@ const MODE_ADVISORIES = { | ||
| sourceAlgorithms: [], | ||
| // Every place an algorithm was seen, as opposed to `sourceAlgorithms`, | ||
| // which is the deduplicated inventory: one row per algorithm:language for | ||
| // the whole tree. Both are wanted, by different callers. A CBOM component | ||
| // list must not repeat a component once per file; a gate reporting a | ||
| // violation must name every file the reader has to change. Before this | ||
| // existed the gate read the inventory, so MD5 in three files was one | ||
| // violation naming the first, and the user found the second only after | ||
| // fixing the first. | ||
| algorithmSites: [], | ||
| tlsFindings: [], | ||
@@ -302,2 +351,12 @@ binaryFindings: [], | ||
| const dbEntry = lookupAlgorithm(algo.algorithm); | ||
| // Recorded before the inventory deduplication, and never subject to it: | ||
| // this is the list of places, and the second and third file are exactly | ||
| // what the deduplication drops. | ||
| results.algorithmSites.push({ | ||
| algorithm: algo.algorithm, | ||
| category: algo.category, | ||
| language, | ||
| file: relPath, | ||
| line: algo.line, | ||
| }); | ||
| const sourceAlgoKey = `${algo.algorithm}:${language}`; | ||
@@ -328,3 +387,3 @@ if (!seenSourceAlgos.has(sourceAlgoKey)) { | ||
| issue: `${algo.algorithm.toUpperCase()}: ${dbEntry.weaknessReason}`, | ||
| severity: dbEntry.quantumRisk === 'critical' ? 'critical' : 'high', | ||
| severity: weakAlgorithmSeverity(dbEntry), | ||
| cwe: dbEntry.cwe, | ||
@@ -362,2 +421,9 @@ fix: dbEntry.replacement ? `Replace with ${dbEntry.replacement}` : undefined, | ||
| for (const weakKey of scanWeakKeySizes(content)) { | ||
| results.algorithmSites.push({ | ||
| algorithm: weakKey.algorithm, | ||
| category: weakKey.category, | ||
| language, | ||
| file: relPath, | ||
| line: weakKey.line, | ||
| }); | ||
| const key = `${weakKey.algorithm}:${language}`; | ||
@@ -364,0 +430,0 @@ if (!seenSourceAlgos.has(key)) { |
+1
-1
| { | ||
| "name": "cryptoserve", | ||
| "version": "0.5.0", | ||
| "version": "0.6.0", | ||
| "description": "CryptoServe CLI - Cryptographic scanning, PQC analysis, encryption, and local key management", | ||
@@ -5,0 +5,0 @@ "type": "module", |
Sorry, the diff of this file is too big to display
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
368042
6.51%7950
5.05%26
4%