New:Socket for Asana Is Now Available.Learn more
Get Started

@mlawsonking/code-guard-mcp

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@mlawsonking/code-guard-mcp - npm Package Compare versions

Comparing version
1.3.0
to
1.4.0
+86
-3
core/engines/email.js

@@ -29,2 +29,78 @@ // GENERATED FILE - do not edit here. Your change will be overwritten.

// A body is not always the text a reader sees. Mail routinely arrives base64 or quoted-printable
// encoded, and multipart mail carries several parts with their own encodings. Scanning the raw block
// meant scanning the encoding, not the message: a base64 `text/plain` body carrying the same
// injection this engine catches in plain text scored zero, because the scanner was reading
// "SWdub3JlIGFsbCBwcmV2aW91cyBpbnN0cnVjdGlvbnM=" and no rule matches that. Decoding is not a nicety
// here; without it the engine does not do the job its name claims.
function decodeTransfer(text, encoding) {
const enc = String(encoding || '').trim().toLowerCase();
const src = String(text || '');
try {
if (enc === 'base64') {
const clean = src.replace(/[^A-Za-z0-9+/=]/g, '');
if (!clean) return '';
return Buffer.from(clean, 'base64').toString('utf8');
}
if (enc === 'quoted-printable') {
return src
.replace(/=\r?\n/g, '')
.replace(/=([0-9A-Fa-f]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
}
} catch { return src; }
return src;
}
// RFC 2047: `Subject: =?utf-8?B?…?=` renders as ordinary text in every mail client, so a brand
// spoof or an instruction can hide in a header the same way it hides in a body.
function decodeEncodedWords(text) {
return String(text || '').replace(
/=\?([^?]+)\?([BbQq])\?([^?]*)\?=/g,
(whole, charset, kind, data) => {
try {
if (/^b$/i.test(kind)) return Buffer.from(data, 'base64').toString('utf8');
return data.replace(/_/g, ' ').replace(/=([0-9A-Fa-f]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
} catch { return whole; }
},
);
}
function contentTypeOf(headers) {
return String(headers['content-type'] || '');
}
function boundaryOf(headers) {
const m = contentTypeOf(headers).match(/boundary\s*=\s*"?([^";\r\n]+)"?/i);
return m ? m[1].trim() : null;
}
// Walk a multipart body into its decoded leaves. Depth-limited because a crafted message can nest
// parts forever, and this runs inside a hook.
function walkParts(body, headers, depth, out) {
if (depth > 6) return;
const boundary = boundaryOf(headers);
const ctype = contentTypeOf(headers).toLowerCase();
if (!boundary || !/^multipart\//.test(ctype.trim())) {
const decoded = decodeTransfer(body, headers['content-transfer-encoding']);
if (!decoded) return;
if (/text\/html/i.test(ctype)) out.html.push(decoded);
else out.text.push(decoded);
return;
}
const marker = `--${boundary}`;
const segments = String(body).split(marker);
for (const segment of segments) {
const chunk = segment.replace(/^\r?\n/, '');
if (!chunk.trim() || chunk.trim() === '--') continue;
const split = chunk.search(/\r?\n\r?\n/);
const partHeaders = parseHeaders(split >= 0 ? chunk.slice(0, split) : chunk);
const partBody = split >= 0 ? chunk.slice(split).replace(/^\r?\n\r?\n/, '') : '';
// A part with no headers of its own is the preamble between boundaries, not a part.
if (split < 0 && !contentTypeOf(partHeaders)) continue;
walkParts(partBody, partHeaders, depth + 1, out);
}
}
function parseEmail(input) {

@@ -36,3 +112,10 @@ let headers = {}, body = '', html = '', raw = '';

headers = parseHeaders(idx >= 0 ? input.slice(0, idx) : input);
body = idx >= 0 ? input.slice(idx).trim() : '';
const rawBody = idx >= 0 ? input.slice(idx).replace(/^\r?\n\r?\n/, '') : '';
const parts = { text: [], html: [] };
walkParts(rawBody, headers, 0, parts);
body = parts.text.join('\n').trim();
html = parts.html.join('\n').trim();
// Keep the undecoded block too when nothing decoded to anything, so a malformed message is still
// scanned rather than silently becoming empty.
if (!body && !html) body = rawBody.trim();
} else if (input && typeof input === 'object') {

@@ -53,3 +136,3 @@ const hsrc = input.headers || {};

const to = parseAddress(headers.to);
const subject = headers.subject || '';
const subject = decodeEncodedWords(headers.subject || '');
const combined = [subject, body, html].filter(Boolean).join('\n');

@@ -162,2 +245,2 @@ return { headers, from, replyTo, returnPath, to, subject, body, html, combined, raw };

module.exports = { parseEmail, parseAddress, parseAuthResults, checkDomainAuth, isDisposable, senderRisk, deliverabilityScan, extractLinks, AUTH_TRUST_NOTE, DKIM_NOT_CHECKED };
module.exports = { parseEmail, decodeTransfer, decodeEncodedWords, parseAddress, parseAuthResults, checkDomainAuth, isDisposable, senderRisk, deliverabilityScan, extractLinks, AUTH_TRUST_NOTE, DKIM_NOT_CHECKED };

@@ -72,2 +72,10 @@ // GENERATED FILE - do not edit here. Your change will be overwritten.

verdict,
// `verdict: allow` says no rule in this set matched. It does not say the text is safe, and the
// difference is the whole product: a paraphrase nobody has written a rule for scores zero here.
// `matched` states the same fact without a word that reads like a safety judgement. It is added
// rather than replacing `verdict`, because callers already branch on that field.
matched: findings.length > 0,
means: findings.length
? 'One or more known patterns matched. See findings for which rule and why.'
: 'No rule in this ruleset matched. That is not a finding of safety: novel or paraphrased wording is not covered by these rules.',
findings,

@@ -74,0 +82,0 @@ categories: [...new Set(findings.map((f) => f.category))],

+19
-7

@@ -320,7 +320,17 @@ // GENERATED FILE - do not edit here. Your change will be overwritten.

const worstSeverity = findings.reduce((a, f) => Math.max(a, f.severity === 'critical' ? 2 : 1), 0);
const verdict = worstSeverity === 2 ? 'danger' : worstSeverity === 1 ? 'caution' : 'safe';
let verdict = worstSeverity === 2 ? 'danger' : worstSeverity === 1 ? 'caution' : 'safe';
// `stamp` downgrades a verdict when a check was skipped, and here four are skipped by design, so
// pass the checks for reporting but keep the verdict this engine actually reached. The honesty is
// carried by `local_only` and by the skipped list itself, both of which the caller must show.
// The four network checks above are skipped by design in this engine, so their absence must not
// move the verdict: that is what `local_only` and the skipped list are for. The comparison list is
// different. It is the ONLY thing this engine checks a name against, it ships inside the package,
// and when it fails to load there is no name check at all. Reporting "safe" then is a check that
// did not run being read as a pass, which is the worst bug this codebase can ship.
//
// This is not hypothetical. Published 0.3.0 omitted `data/` from its npm `files` list, so every
// install answered `safe` for `crossenv`, a real npm typosquat attack. The `files` entry is fixed
// and a tarball test now guards it; this is the second lock, so that a list which cannot load can
// never again read as a clean name.
const listMissing = !list.available;
if (listMissing && verdict === 'safe') verdict = 'unknown';
const out = stamp({

@@ -333,5 +343,7 @@ ok: true,

local_only: true,
summary: verdict === 'safe'
? `Nothing in the name "${raw}" resembles a known popular package closely enough to flag. The registry, OSV and the package contents were not consulted, so this is not a statement that the package is safe.`
: findings.map((f) => f.message).join(' '),
summary: listMissing
? `The bundled list of popular package names could not be loaded, so the name "${raw}" was not compared against anything. This is not a result: nothing was checked.`
: verdict === 'safe'
? `Nothing in the name "${raw}" resembles a known popular package closely enough to flag. The registry, OSV and the package contents were not consulted, so this is not a statement that the package is safe.`
: findings.map((f) => f.message).join(' '),
list_generated: list.available ? list.generated : null,

@@ -338,0 +350,0 @@ list_size: list.available ? list.size : 0,

@@ -64,2 +64,5 @@ // GENERATED FILE - do not edit here. Your change will be overwritten.

// Addresses that are reserved by RFC and cannot resolve to a real mailbox.
const RESERVED_DOMAIN = /@(?:[A-Za-z0-9.-]*\.)?(?:test|example|invalid|localhost)$|@(?:[A-Za-z0-9.-]*\.)?example\.(?:com|net|org)$/i;
const PII_RULES = [

@@ -103,2 +106,7 @@ { id: 'email', type: 'Email', re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, severity: 'low', vg: 0 },

if (r.luhn && !luhn(val)) { if (!r.re.global) break; continue; }
// RFC 2606 and 6761 set aside .test, .example, .invalid and .localhost (and example.com/net/org)
// precisely so documentation and tests have addresses that can never belong to anyone. Calling
// those personal data is noise, and noise is what gets a scanner switched off. This engine's
// own test fixtures tripped it.
if (r.id === 'email' && RESERVED_DOMAIN.test(val)) { if (!r.re.global) break; continue; }
const start = vg === 0 ? m.index : m.index + m[0].indexOf(val);

@@ -105,0 +113,0 @@ spans.push({ start, end: start + val.length, type: r.type });

@@ -19,3 +19,5 @@ // GENERATED FILE - do not edit here. Your change will be overwritten.

const ORDER = { allow: 0, clear: 0, ok: 0, caution: 1, review: 1, warn: 1, block: 2, danger: 2 };
// `safe` and `unknown` are listed explicitly rather than defaulted: `safe` is a pass, and `unknown`
// means a check could not run, which must sort with caution so it can never be mistaken for one.
const ORDER = { allow: 0, clear: 0, ok: 0, safe: 0, unknown: 1, caution: 1, review: 1, warn: 1, block: 2, danger: 2 };

@@ -22,0 +24,0 @@ // A verdict may only ever move toward caution, never away from it.

{
"name": "@mlawsonking/code-guard-mcp",
"version": "1.3.0",
"version": "1.4.0",
"mcpName": "io.github.mlawsonking/code-guard-mcp",

@@ -5,0 +5,0 @@ "publishConfig": {