Sign In

cryptoserve

Package Overview
Dependencies
Maintainers
1
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

cryptoserve - npm Package Compare versions

Comparing version
0.5.0
to
0.6.0
+96
-6
lib/sarif.mjs

@@ -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),

@@ -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