mcptrustchecker 1.0.0

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.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +379 -0
  3. package/dist/acquire/clientConfig.d.ts +47 -0
  4. package/dist/acquire/clientConfig.js +124 -0
  5. package/dist/acquire/discover.d.ts +12 -0
  6. package/dist/acquire/discover.js +53 -0
  7. package/dist/acquire/index.d.ts +24 -0
  8. package/dist/acquire/index.js +175 -0
  9. package/dist/acquire/live.d.ts +47 -0
  10. package/dist/acquire/live.js +357 -0
  11. package/dist/acquire/manifest.d.ts +10 -0
  12. package/dist/acquire/manifest.js +94 -0
  13. package/dist/acquire/npm.d.ts +10 -0
  14. package/dist/acquire/npm.js +55 -0
  15. package/dist/acquire/source.d.ts +16 -0
  16. package/dist/acquire/source.js +109 -0
  17. package/dist/cli/index.d.ts +15 -0
  18. package/dist/cli/index.js +396 -0
  19. package/dist/config.d.ts +15 -0
  20. package/dist/config.js +81 -0
  21. package/dist/data/capabilityLexicon.d.ts +51 -0
  22. package/dist/data/capabilityLexicon.js +204 -0
  23. package/dist/data/confusables.d.ts +17 -0
  24. package/dist/data/confusables.js +65 -0
  25. package/dist/data/injectionPatterns.d.ts +55 -0
  26. package/dist/data/injectionPatterns.js +216 -0
  27. package/dist/data/knownCves.d.ts +24 -0
  28. package/dist/data/knownCves.js +49 -0
  29. package/dist/data/protectedPackages.d.ts +30 -0
  30. package/dist/data/protectedPackages.js +89 -0
  31. package/dist/data/ruleCatalog.d.ts +16 -0
  32. package/dist/data/ruleCatalog.js +94 -0
  33. package/dist/data/sourcePatterns.d.ts +25 -0
  34. package/dist/data/sourcePatterns.js +92 -0
  35. package/dist/data/unicode.d.ts +34 -0
  36. package/dist/data/unicode.js +99 -0
  37. package/dist/detectors/capability.d.ts +14 -0
  38. package/dist/detectors/capability.js +281 -0
  39. package/dist/detectors/collision.d.ts +11 -0
  40. package/dist/detectors/collision.js +63 -0
  41. package/dist/detectors/flowGraph.d.ts +43 -0
  42. package/dist/detectors/flowGraph.js +100 -0
  43. package/dist/detectors/index.d.ts +18 -0
  44. package/dist/detectors/index.js +26 -0
  45. package/dist/detectors/injection.d.ts +9 -0
  46. package/dist/detectors/injection.js +178 -0
  47. package/dist/detectors/meta.d.ts +8 -0
  48. package/dist/detectors/meta.js +33 -0
  49. package/dist/detectors/posture.d.ts +9 -0
  50. package/dist/detectors/posture.js +128 -0
  51. package/dist/detectors/source.d.ts +12 -0
  52. package/dist/detectors/source.js +87 -0
  53. package/dist/detectors/supplyChain.d.ts +17 -0
  54. package/dist/detectors/supplyChain.js +243 -0
  55. package/dist/detectors/toxicFlow.d.ts +22 -0
  56. package/dist/detectors/toxicFlow.js +201 -0
  57. package/dist/detectors/unicode.d.ts +21 -0
  58. package/dist/detectors/unicode.js +244 -0
  59. package/dist/engine.d.ts +27 -0
  60. package/dist/engine.js +135 -0
  61. package/dist/index.d.ts +38 -0
  62. package/dist/index.js +46 -0
  63. package/dist/lockfile.d.ts +30 -0
  64. package/dist/lockfile.js +87 -0
  65. package/dist/policy.d.ts +13 -0
  66. package/dist/policy.js +48 -0
  67. package/dist/report/badge.d.ts +14 -0
  68. package/dist/report/badge.js +18 -0
  69. package/dist/report/json.d.ts +4 -0
  70. package/dist/report/json.js +4 -0
  71. package/dist/report/markdown.d.ts +6 -0
  72. package/dist/report/markdown.js +110 -0
  73. package/dist/report/sarif.d.ts +7 -0
  74. package/dist/report/sarif.js +105 -0
  75. package/dist/report/terminal.d.ts +15 -0
  76. package/dist/report/terminal.js +252 -0
  77. package/dist/scoring/capability.d.ts +10 -0
  78. package/dist/scoring/capability.js +48 -0
  79. package/dist/scoring/index.d.ts +10 -0
  80. package/dist/scoring/index.js +96 -0
  81. package/dist/scoring/model.d.ts +50 -0
  82. package/dist/scoring/model.js +101 -0
  83. package/dist/types.d.ts +361 -0
  84. package/dist/types.js +12 -0
  85. package/dist/util/ansi.d.ts +26 -0
  86. package/dist/util/ansi.js +42 -0
  87. package/dist/util/capabilities.d.ts +17 -0
  88. package/dist/util/capabilities.js +68 -0
  89. package/dist/util/distance.d.ts +23 -0
  90. package/dist/util/distance.js +119 -0
  91. package/dist/util/hash.d.ts +20 -0
  92. package/dist/util/hash.js +80 -0
  93. package/dist/util/text.d.ts +45 -0
  94. package/dist/util/text.js +170 -0
  95. package/dist/version.d.ts +13 -0
  96. package/dist/version.js +13 -0
  97. package/package.json +72 -0
package/dist/policy.js ADDED
@@ -0,0 +1,48 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Policy-as-code. A team declares what "acceptable" means once (in
4
+ * `mcptrustchecker.config.json` under `policy`) and every scan is gated against
5
+ * it in CI — beyond a single grade/score threshold. Pure and deterministic.
6
+ */
7
+ import { GRADE_RANK } from './scoring/model.js';
8
+ const CAP_ORDER = ['minimal', 'moderate', 'high', 'critical'];
9
+ /** Evaluate a report against a policy; empty array means it passes. */
10
+ export function evaluatePolicy(report, policy) {
11
+ if (!policy)
12
+ return [];
13
+ const violations = [];
14
+ // Normalize case so a miscased config value ("b", "Critical") can't silently
15
+ // fail OPEN (disabling the gate) or fail CLOSED (blocking every scan).
16
+ const minGrade = policy.minGrade ? String(policy.minGrade).toUpperCase() : undefined;
17
+ if (minGrade && minGrade in GRADE_RANK && GRADE_RANK[report.score.grade] < GRADE_RANK[minGrade]) {
18
+ violations.push({
19
+ policy: 'minGrade',
20
+ message: `Trust grade ${report.score.grade} is worse than the required minimum ${minGrade}.`,
21
+ });
22
+ }
23
+ const maxCapability = policy.maxCapability ? String(policy.maxCapability).toLowerCase() : undefined;
24
+ if (maxCapability && CAP_ORDER.includes(maxCapability)) {
25
+ const level = report.capabilityProfile.level;
26
+ if (CAP_ORDER.indexOf(level) > CAP_ORDER.indexOf(maxCapability)) {
27
+ violations.push({
28
+ policy: 'maxCapability',
29
+ message: `Capability level "${level}" exceeds the allowed maximum "${maxCapability}".`,
30
+ });
31
+ }
32
+ }
33
+ if (policy.denyRules?.length) {
34
+ const denied = new Set(policy.denyRules);
35
+ const hit = [...new Set(report.findings.map((f) => f.ruleId).filter((id) => denied.has(id)))];
36
+ if (hit.length) {
37
+ violations.push({ policy: 'denyRules', message: `Denied rule(s) fired: ${hit.join(', ')}.` });
38
+ }
39
+ }
40
+ if (policy.denyCapabilities?.length) {
41
+ const present = new Set(report.capabilityProfile.tags);
42
+ const hit = policy.denyCapabilities.filter((t) => present.has(t));
43
+ if (hit.length) {
44
+ violations.push({ policy: 'denyCapabilities', message: `Denied capability(ies) present: ${hit.join(', ')}.` });
45
+ }
46
+ }
47
+ return violations;
48
+ }
@@ -0,0 +1,14 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Shields.io "endpoint" badge JSON. Host the output somewhere and point a
4
+ * shields.io endpoint badge at it, or let the marketplace render it directly:
5
+ * ![trust](https://img.shields.io/endpoint?url=<raw-json-url>)
6
+ */
7
+ import type { ScanReport } from '../types.js';
8
+ export interface ShieldsBadge {
9
+ schemaVersion: 1;
10
+ label: string;
11
+ message: string;
12
+ color: string;
13
+ }
14
+ export declare function renderBadge(report: ScanReport, label?: string): string;
@@ -0,0 +1,18 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Shields.io "endpoint" badge JSON. Host the output somewhere and point a
4
+ * shields.io endpoint badge at it, or let the marketplace render it directly:
5
+ * ![trust](https://img.shields.io/endpoint?url=<raw-json-url>)
6
+ */
7
+ function color(grade) {
8
+ return { A: 'brightgreen', B: 'green', C: 'yellow', D: 'orange', F: 'red' }[grade];
9
+ }
10
+ export function renderBadge(report, label = 'mcptrustchecker') {
11
+ const badge = {
12
+ schemaVersion: 1,
13
+ label,
14
+ message: `${report.score.grade} (${report.score.score})`,
15
+ color: color(report.score.grade),
16
+ };
17
+ return JSON.stringify(badge);
18
+ }
@@ -0,0 +1,4 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ import type { ScanReport } from '../types.js';
3
+ /** Pretty JSON report (the machine-readable canonical form). */
4
+ export declare function renderJson(report: ScanReport): string;
@@ -0,0 +1,4 @@
1
+ /** Pretty JSON report (the machine-readable canonical form). */
2
+ export function renderJson(report) {
3
+ return JSON.stringify(report, null, 2);
4
+ }
@@ -0,0 +1,6 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Markdown report — for PR comments, job summaries, and README embeds.
4
+ */
5
+ import type { ScanReport } from '../types.js';
6
+ export declare function renderMarkdown(report: ScanReport): string;
@@ -0,0 +1,110 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Markdown report — for PR comments, job summaries, and README embeds.
4
+ */
5
+ import { ALL_CATEGORIES } from '../scoring/model.js';
6
+ const SEV_EMOJI = {
7
+ critical: '🟥',
8
+ high: '🟧',
9
+ medium: '🟨',
10
+ low: '🟦',
11
+ info: '⬜',
12
+ };
13
+ function gradeEmoji(grade) {
14
+ return { A: '🟢', B: '🟢', C: '🟡', D: '🟠', F: '🔴' }[grade] ?? '⚪';
15
+ }
16
+ export function renderMarkdown(report) {
17
+ const s = report.score;
18
+ const md = [];
19
+ const capEmoji = { minimal: '🟢', moderate: '🔵', high: '🟠', critical: '🔴' }[report.capabilityProfile.level] ?? '⚪';
20
+ md.push(`## ${gradeEmoji(s.grade)} MCP Trust Checker — Trust ${s.grade} (${s.score}/100) · ${capEmoji} Capability ${report.capabilityProfile.level.toUpperCase()}`);
21
+ md.push('');
22
+ md.push(`> **Trust** = malice/negligence signals (drives the grade) · **Capability** = blast radius if the model is manipulated (not a verdict).`);
23
+ if (report.capabilityProfile.reasons.length) {
24
+ md.push('>');
25
+ md.push(`> Capability: ${report.capabilityProfile.reasons.join('; ')}.`);
26
+ }
27
+ md.push('');
28
+ md.push(`**Target:** \`${mdInline(report.target.id)}\` (${report.target.source.kind}) · ` +
29
+ `**Surface:** ${report.stats.tools} tools, ${report.stats.prompts} prompts, ${report.stats.resources} resources · ` +
30
+ `**Methodology:** \`${s.methodologyVersion}\``);
31
+ md.push('');
32
+ const bs = report.stats.findingsBySeverity;
33
+ md.push(`**Findings:** ${bs.critical} critical · ${bs.high} high · ${bs.medium} medium · ${bs.low} low · ${bs.info} info`);
34
+ md.push('');
35
+ if (s.gatesFired.length) {
36
+ md.push('> [!WARNING]');
37
+ for (const g of s.gatesFired)
38
+ md.push(`> ${g}`);
39
+ md.push('');
40
+ }
41
+ const cats = ALL_CATEGORIES.filter((c) => s.categorySubtotals[c] > 0);
42
+ if (cats.length) {
43
+ md.push('| Category | Penalty |');
44
+ md.push('| --- | ---: |');
45
+ for (const c of cats)
46
+ md.push(`| ${c} | -${s.categorySubtotals[c]} |`);
47
+ md.push('');
48
+ }
49
+ if (report.toxicFlows.length) {
50
+ md.push('### Toxic flows');
51
+ md.push('');
52
+ for (const f of report.toxicFlows) {
53
+ md.push(`- ${SEV_EMOJI[f.severity]} **${f.severity}** — ${mdInline(f.description)}`);
54
+ }
55
+ md.push('');
56
+ }
57
+ if (report.integrity && report.integrity.status === 'drift') {
58
+ md.push('### ⚠️ Integrity drift (possible rug pull)');
59
+ md.push('');
60
+ for (const ch of report.integrity.changes ?? [])
61
+ md.push(`- ${mdInline(ch.detail)}`);
62
+ md.push('');
63
+ }
64
+ if (report.findings.length) {
65
+ md.push('### Findings');
66
+ md.push('');
67
+ md.push('| Sev | Rule | Finding | Location |');
68
+ md.push('| --- | --- | --- | --- |');
69
+ for (const f of report.findings) {
70
+ const loc = f.location
71
+ ? `${f.location.kind}${f.location.name ? ` \`${cell(f.location.name)}\`` : ''}`
72
+ : '';
73
+ md.push(`| ${SEV_EMOJI[f.severity]} ${f.severity} | \`${cell(f.ruleId)}\` | ${cell(f.title)} | ${loc} |`);
74
+ }
75
+ md.push('');
76
+ }
77
+ else {
78
+ md.push('_No findings. This surface looks clean by the current ruleset._');
79
+ md.push('');
80
+ }
81
+ md.push(`<sub>Scanned with [MCP Trust Checker](https://github.com/illiahaidar/mcptrustchecker) ${report.tool.version} · digest \`${report.surfaceDigest.slice(0, 12)}…\`</sub>`);
82
+ return md.join('\n');
83
+ }
84
+ /**
85
+ * Make an untrusted string safe for inline Markdown prose (list items, code
86
+ * spans). Kills newlines/control (so it can't break out into a forged heading),
87
+ * neutralizes code spans and raw HTML, and escapes link brackets (so a
88
+ * `[approve](evil)` phishing link can't render). Tool names/descriptions reach
89
+ * these paths unfiltered, so this is a security boundary for PR-comment output.
90
+ */
91
+ function mdInline(s) {
92
+ return String(s ?? '')
93
+ // eslint-disable-next-line no-control-regex
94
+ .replace(/[\u0000-\u001f\u007f-\u009f]+/g, ' ')
95
+ .replace(/`/g, '‘')
96
+ .replace(/[<>]/g, '')
97
+ .replace(/[[\]]/g, '\\$&')
98
+ .trim();
99
+ }
100
+ /** Make an untrusted string safe for a single Markdown table cell. */
101
+ function cell(s) {
102
+ return String(s)
103
+ // eslint-disable-next-line no-control-regex
104
+ .replace(/[\u0000-\u001f\u007f-\u009f]+/g, ' ') // newlines & control chars break the row
105
+ .replace(/\|/g, '\\|')
106
+ .replace(/`/g, '‘')
107
+ .replace(/[<>]/g, '')
108
+ .replace(/[[\]]/g, '\\$&') // link brackets → no phishing link renders in a cell
109
+ .trim();
110
+ }
@@ -0,0 +1,7 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * SARIF 2.1.0 output for GitHub code scanning ("Security" tab) and any SARIF
4
+ * consumer. Each unique rule becomes a reportingDescriptor; each finding a result.
5
+ */
6
+ import type { ScanReport } from '../types.js';
7
+ export declare function renderSarif(report: ScanReport): string;
@@ -0,0 +1,105 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * SARIF 2.1.0 output for GitHub code scanning ("Security" tab) and any SARIF
4
+ * consumer. Each unique rule becomes a reportingDescriptor; each finding a result.
5
+ */
6
+ import { TOOL_VERSION } from '../version.js';
7
+ function level(sev) {
8
+ switch (sev) {
9
+ case 'critical':
10
+ case 'high':
11
+ return 'error';
12
+ case 'medium':
13
+ return 'warning';
14
+ case 'low':
15
+ return 'note';
16
+ case 'info':
17
+ return 'none';
18
+ }
19
+ }
20
+ function securitySeverity(sev) {
21
+ // GitHub uses a 0-10 scale to sort the Security tab.
22
+ switch (sev) {
23
+ case 'critical':
24
+ return '9.5';
25
+ case 'high':
26
+ return '8.0';
27
+ case 'medium':
28
+ return '5.0';
29
+ case 'low':
30
+ return '3.0';
31
+ case 'info':
32
+ return '0.0';
33
+ }
34
+ }
35
+ export function renderSarif(report) {
36
+ const rulesById = new Map();
37
+ for (const f of report.findings)
38
+ if (!rulesById.has(f.ruleId))
39
+ rulesById.set(f.ruleId, f);
40
+ const rules = [...rulesById.values()].map((f) => ({
41
+ id: f.ruleId,
42
+ name: f.ruleId,
43
+ shortDescription: { text: f.title },
44
+ fullDescription: { text: f.description.split('\n')[0] },
45
+ helpUri: f.references?.[0],
46
+ defaultConfiguration: { level: level(f.severity) },
47
+ properties: {
48
+ category: f.category,
49
+ 'security-severity': securitySeverity(f.severity),
50
+ tags: ['security', 'mcp', f.category, ...(f.owasp ? [f.owasp] : [])],
51
+ },
52
+ }));
53
+ const results = report.findings.map((f) => ({
54
+ ruleId: f.ruleId,
55
+ level: level(f.severity),
56
+ message: {
57
+ text: `${f.title}. ${f.description.split('\n')[0]}` +
58
+ (f.evidence ? ` [evidence: ${f.evidence.slice(0, 120)}]` : '') +
59
+ (f.remediation ? ` Remediation: ${f.remediation}` : ''),
60
+ },
61
+ locations: [
62
+ {
63
+ logicalLocations: [
64
+ {
65
+ name: f.location?.name ?? report.target.id,
66
+ kind: f.location?.kind ?? 'server',
67
+ fullyQualifiedName: [report.target.id, f.location?.kind, f.location?.name, f.location?.field]
68
+ .filter(Boolean)
69
+ .join('/'),
70
+ },
71
+ ],
72
+ },
73
+ ],
74
+ properties: {
75
+ confidence: f.confidence,
76
+ category: f.category,
77
+ owasp: f.owasp,
78
+ },
79
+ }));
80
+ const sarif = {
81
+ $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
82
+ version: '2.1.0',
83
+ runs: [
84
+ {
85
+ tool: {
86
+ driver: {
87
+ name: 'MCP Trust Checker',
88
+ version: TOOL_VERSION,
89
+ informationUri: 'https://github.com/illiahaidar/mcptrustchecker',
90
+ rules,
91
+ },
92
+ },
93
+ results,
94
+ properties: {
95
+ trustScore: report.score.score,
96
+ grade: report.score.grade,
97
+ capabilityLevel: report.capabilityProfile.level,
98
+ methodologyVersion: report.score.methodologyVersion,
99
+ surfaceDigest: report.surfaceDigest,
100
+ },
101
+ },
102
+ ],
103
+ };
104
+ return JSON.stringify(sarif, null, 2);
105
+ }
@@ -0,0 +1,15 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Human-facing terminal report. Zero-dependency ANSI, degrades to plain text
4
+ * when piped or under NO_COLOR.
5
+ */
6
+ import type { ScanReport } from '../types.js';
7
+ export declare function renderTerminal(report: ScanReport, opts?: {
8
+ details?: boolean;
9
+ }): string;
10
+ /**
11
+ * Strip control & escape sequences from attacker-controlled text before printing
12
+ * it. Tool metadata is untrusted; echoing its raw ANSI/OSC/control bytes would
13
+ * let a malicious server rewrite the terminal (consent phishing / title spoof).
14
+ */
15
+ export declare function clean(s: unknown): string;
@@ -0,0 +1,252 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Human-facing terminal report. Zero-dependency ANSI, degrades to plain text
4
+ * when piped or under NO_COLOR.
5
+ */
6
+ import { c, padVisible } from '../util/ansi.js';
7
+ import { ALL_CATEGORIES, isCapabilityRule } from '../scoring/model.js';
8
+ const SEV_LABEL = {
9
+ critical: (s) => c.bold(c.red(s)),
10
+ high: (s) => c.red(s),
11
+ medium: (s) => c.yellow(s),
12
+ low: (s) => c.cyan(s),
13
+ info: (s) => c.gray(s),
14
+ };
15
+ const SEV_TAG = {
16
+ critical: 'CRIT',
17
+ high: 'HIGH',
18
+ medium: 'MED ',
19
+ low: 'LOW ',
20
+ info: 'INFO',
21
+ };
22
+ function gradeColor(grade, s) {
23
+ switch (grade) {
24
+ case 'A':
25
+ return c.bold(c.green(s));
26
+ case 'B':
27
+ return c.bold(c.green(s));
28
+ case 'C':
29
+ return c.bold(c.yellow(s));
30
+ case 'D':
31
+ return c.bold(c.yellow(s));
32
+ case 'F':
33
+ return c.bold(c.red(s));
34
+ }
35
+ }
36
+ function capColor(level, s) {
37
+ switch (level) {
38
+ case 'minimal':
39
+ return c.green(s);
40
+ case 'moderate':
41
+ return c.cyan(s);
42
+ case 'high':
43
+ return c.yellow(s);
44
+ case 'critical':
45
+ return c.bold(c.red(s));
46
+ }
47
+ }
48
+ /** Usable terminal width (clamped), for wrapping detailed descriptions. */
49
+ const TERM = Math.max(64, Math.min(process.stdout.columns || 100, 118));
50
+ function line(char = '─', width = Math.min(TERM, 72)) {
51
+ return c.gray(char.repeat(width));
52
+ }
53
+ /** Word-wrap a string to a width, preserving explicit newlines. */
54
+ function wrap(text, width) {
55
+ const out = [];
56
+ for (const para of text.split('\n')) {
57
+ const words = para.replace(/\s+/g, ' ').trim().split(' ');
58
+ let cur = '';
59
+ for (const w of words) {
60
+ if (cur && (cur + ' ' + w).length > width) {
61
+ out.push(cur);
62
+ cur = w;
63
+ }
64
+ else {
65
+ cur = cur ? `${cur} ${w}` : w;
66
+ }
67
+ }
68
+ out.push(cur);
69
+ }
70
+ return out;
71
+ }
72
+ const LABEL_W = 9;
73
+ /** A labeled, wrapped field with a hanging indent aligned under the text column. */
74
+ function labeled(label, text, color) {
75
+ const head = ` ${c.gray(padVisible(label, LABEL_W))}`;
76
+ const cont = ' '.repeat(3 + LABEL_W);
77
+ const lines = wrap(text, TERM - 3 - LABEL_W);
78
+ return lines.map((ln, i) => (i === 0 ? head : cont) + color(ln)).join('\n');
79
+ }
80
+ export function renderTerminal(report, opts = {}) {
81
+ const out = [];
82
+ const { score } = report;
83
+ const threatFindings = report.findings.filter((f) => !isCapabilityRule(f.ruleId) && f.severity !== 'info');
84
+ const capabilityFindings = report.findings.filter((f) => isCapabilityRule(f.ruleId));
85
+ out.push('');
86
+ out.push(`${c.bold('MCP Trust Checker')} ${c.gray('· MCP security scan')}`);
87
+ out.push(line());
88
+ out.push(`${c.gray('Target ')} ${clean(report.target.id)} ${c.gray(`(${report.target.source.kind})`)}`);
89
+ if (report.target.server?.name) {
90
+ out.push(`${c.gray('Server ')} ${clean(report.target.server.name)}${report.target.server.version ? ` v${clean(report.target.server.version)}` : ''}`);
91
+ }
92
+ out.push(`${c.gray('Surface')} ${report.stats.tools} tools · ${report.stats.prompts} prompts · ${report.stats.resources} resources`);
93
+ out.push('');
94
+ // Grade badge (TRUST) + capability level (BLAST RADIUS) — two axes.
95
+ const badge = ` GRADE ${score.grade} `;
96
+ const cap = report.capabilityProfile;
97
+ out.push(` ${gradeColor(score.grade, `╭${'─'.repeat(badge.length)}╮`)}`);
98
+ out.push(` ${gradeColor(score.grade, `│${badge}│`)} ${c.bold(`Trust Score ${score.score}/100`)} ${c.gray('(malice/negligence signals)')}`);
99
+ out.push(` ${gradeColor(score.grade, `╰${'─'.repeat(badge.length)}╯`)} ${capColor(cap.level, `Capability ${cap.level.toUpperCase()}`)} ${c.gray('(blast radius)')}`);
100
+ if (cap.reasons.length) {
101
+ out.push(` ${c.gray('methodology ' + score.methodologyVersion)} · ${c.gray(cap.reasons.slice(0, 2).join('; '))}`);
102
+ }
103
+ else {
104
+ out.push(` ${c.gray('methodology ' + score.methodologyVersion)}`);
105
+ }
106
+ out.push('');
107
+ // Threat severity counts (what drives the grade) + capability count.
108
+ const tc = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
109
+ for (const f of threatFindings)
110
+ tc[f.severity] += 1;
111
+ const counts = ['critical', 'high', 'medium', 'low']
112
+ .filter((s) => tc[s] > 0)
113
+ .map((s) => SEV_LABEL[s](`${tc[s]} ${s}`))
114
+ .join(c.gray(' · '));
115
+ out.push(`${c.gray('Threats ')} ${counts || c.green('none')}`);
116
+ out.push(`${c.gray('Capability')} ${capColor(cap.level, cap.level)} ${c.gray(`(${capabilityFindings.length} observation${capabilityFindings.length === 1 ? '' : 's'})`)}`);
117
+ // Category penalties.
118
+ const cats = ALL_CATEGORIES.filter((cat) => score.categorySubtotals[cat] > 0);
119
+ if (cats.length) {
120
+ out.push('');
121
+ out.push(c.gray('Penalty by category'));
122
+ for (const cat of cats) {
123
+ const pts = score.categorySubtotals[cat];
124
+ out.push(` ${padVisible(cat, 16)} ${c.red(`-${pts}`)}`);
125
+ }
126
+ }
127
+ // Gates.
128
+ if (score.gatesFired.length) {
129
+ out.push('');
130
+ out.push(c.gray('Hard gates'));
131
+ for (const g of score.gatesFired)
132
+ out.push(` ${c.magenta('▸')} ${g}`);
133
+ }
134
+ // Toxic flows.
135
+ if (report.toxicFlows.length) {
136
+ out.push('');
137
+ out.push(`${c.bold('Toxic flows')} ${c.gray('(untrusted-input → sensitive-source → external-sink)')}`);
138
+ for (const f of report.toxicFlows) {
139
+ out.push(` ${SEV_LABEL[f.severity](`[${f.severity}]`)} ${clean(f.description)}` +
140
+ (f.selfContained ? c.gray(' (single tool)') : ''));
141
+ }
142
+ }
143
+ // Integrity.
144
+ if (report.integrity) {
145
+ out.push('');
146
+ const st = report.integrity.status;
147
+ const label = st === 'drift'
148
+ ? c.bold(c.red('DRIFT — surface changed since pin'))
149
+ : st === 'unchanged'
150
+ ? c.green('unchanged (matches pin)')
151
+ : c.gray('first seen (not yet pinned)');
152
+ out.push(`${c.gray('Integrity')} ${label}`);
153
+ for (const ch of report.integrity.changes ?? [])
154
+ out.push(` ${c.red('▸')} ${clean(ch.detail)}`);
155
+ }
156
+ // THREAT findings — drive the Trust grade. Detailed, most severe first.
157
+ if (report.findings.length) {
158
+ out.push('');
159
+ out.push(line());
160
+ out.push(`${c.bold('Threat findings')} ${c.gray('— affect the Trust grade (most severe first)')}`);
161
+ if (threatFindings.length === 0) {
162
+ out.push('');
163
+ out.push(` ${c.green('none')} ${c.gray('— no malice/negligence signals detected')}`);
164
+ }
165
+ let lastSev = null;
166
+ for (const f of threatFindings) {
167
+ if (f.severity !== lastSev) {
168
+ out.push('');
169
+ out.push(c.gray(`── ${f.severity.toUpperCase()} ──`));
170
+ lastSev = f.severity;
171
+ }
172
+ out.push('');
173
+ out.push(renderFinding(f, opts.details ?? false));
174
+ }
175
+ // CAPABILITY observations — blast radius, NOT a verdict on trust.
176
+ if (capabilityFindings.length) {
177
+ out.push('');
178
+ out.push(line());
179
+ out.push(`${c.bold('Capability observations')} ${c.gray('— what this server can do; informational, not a trust verdict')}`);
180
+ if (opts.details) {
181
+ let last = null;
182
+ for (const f of capabilityFindings) {
183
+ if (f.severity !== last) {
184
+ out.push('');
185
+ out.push(c.gray(`── ${f.severity.toUpperCase()} ──`));
186
+ last = f.severity;
187
+ }
188
+ out.push('');
189
+ out.push(renderFinding(f, true));
190
+ }
191
+ }
192
+ else {
193
+ out.push('');
194
+ for (const f of capabilityFindings) {
195
+ out.push(` ${SEV_LABEL[f.severity](`[${SEV_TAG[f.severity].trim()}]`)} ${c.gray(f.ruleId)} ${clean(f.title)}`);
196
+ }
197
+ out.push('');
198
+ out.push(c.gray(' (run with --details for the full description of each)'));
199
+ }
200
+ }
201
+ }
202
+ else {
203
+ out.push('');
204
+ out.push(c.green('No findings. This surface looks clean by the current ruleset.'));
205
+ }
206
+ out.push('');
207
+ out.push(line());
208
+ out.push(c.gray(`${report.findings.length} finding(s) · digest ${report.surfaceDigest.slice(0, 12)}… · ` +
209
+ `${report.tool.name} ${report.tool.version}`));
210
+ out.push('');
211
+ return out.join('\n');
212
+ }
213
+ function locationString(f) {
214
+ if (!f.location)
215
+ return undefined;
216
+ const l = f.location;
217
+ return `${l.kind}${l.name ? ` "${l.name}"` : ''}${l.field ? ` → ${l.field}` : ''}`;
218
+ }
219
+ /**
220
+ * Strip control & escape sequences from attacker-controlled text before printing
221
+ * it. Tool metadata is untrusted; echoing its raw ANSI/OSC/control bytes would
222
+ * let a malicious server rewrite the terminal (consent phishing / title spoof).
223
+ */
224
+ export function clean(s) {
225
+ // eslint-disable-next-line no-control-regex
226
+ return String(s ?? '').replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ');
227
+ }
228
+ function renderFinding(f, details) {
229
+ const lines = [];
230
+ // Header: [SEV · confidence] RULE-ID Title
231
+ lines.push(`${SEV_LABEL[f.severity](`[${SEV_TAG[f.severity].trim()} · ${f.confidence}]`)} ` +
232
+ `${c.gray(f.ruleId)} ${c.bold(clean(f.title))}`);
233
+ const loc = locationString(f);
234
+ if (loc)
235
+ lines.push(labeled('where', clean(loc), c.gray));
236
+ // Full (wrapped) description — the "more detail" the report is about.
237
+ lines.push(labeled('what', clean(f.description), c.white));
238
+ if (f.evidence)
239
+ lines.push(labeled('evidence', clean(details ? f.evidence : truncate(f.evidence, 200)), c.yellow));
240
+ if (f.remediation)
241
+ lines.push(labeled('fix', clean(f.remediation), c.green));
242
+ const maps = [f.owasp, `category: ${f.category}`].filter(Boolean).join(' · ');
243
+ if (maps)
244
+ lines.push(labeled('maps', maps, c.gray));
245
+ if (details && f.references?.length)
246
+ lines.push(labeled('refs', f.references.join(' '), c.blue));
247
+ return lines.join('\n');
248
+ }
249
+ function truncate(s, n) {
250
+ const flat = String(s).replace(/\s+/g, ' ').trim();
251
+ return flat.length > n ? `${flat.slice(0, n)}…` : flat;
252
+ }
@@ -0,0 +1,10 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Capability (blast-radius) rating — the second axis, independent of the trust
4
+ * grade. It answers "how much could this server do if the model driving it were
5
+ * prompt-injected?" A legitimate, well-behaved server can still be high- or
6
+ * critical-capability; that is not a mark against its trustworthiness, it is
7
+ * information for deciding how much access to grant.
8
+ */
9
+ import type { CapabilityProfile, ToolCapability, ToxicFlow } from '../types.js';
10
+ export declare function computeCapabilityProfile(capabilities: ToolCapability[], flows: ToxicFlow[]): CapabilityProfile;
@@ -0,0 +1,48 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Capability (blast-radius) rating — the second axis, independent of the trust
4
+ * grade. It answers "how much could this server do if the model driving it were
5
+ * prompt-injected?" A legitimate, well-behaved server can still be high- or
6
+ * critical-capability; that is not a mark against its trustworthiness, it is
7
+ * information for deciding how much access to grant.
8
+ */
9
+ const ORDER = ['minimal', 'moderate', 'high', 'critical'];
10
+ function atLeast(current, candidate) {
11
+ return ORDER.indexOf(candidate) > ORDER.indexOf(current) ? candidate : current;
12
+ }
13
+ export function computeCapabilityProfile(capabilities, flows) {
14
+ const tagSet = new Set(capabilities.flatMap((c) => c.tags));
15
+ const has = (t) => tagSet.has(t);
16
+ const reasons = [];
17
+ let level = 'minimal';
18
+ const bump = (candidate, reason) => {
19
+ level = atLeast(level, candidate);
20
+ reasons.push(reason);
21
+ };
22
+ if (has('untrusted-input'))
23
+ bump('moderate', 'ingests untrusted external content (a prompt-injection entry point)');
24
+ if (has('external-sink'))
25
+ bump('moderate', 'can send data / act on an external service');
26
+ if (has('file-write'))
27
+ bump('moderate', 'can create, modify or delete files');
28
+ if (has('sensitive-source'))
29
+ bump('moderate', 'reads sensitive or local data');
30
+ if (has('sensitive-source') && (has('external-sink') || has('code-exec'))) {
31
+ bump('high', 'has a read → egress path (a data-exfiltration surface)');
32
+ }
33
+ if (has('code-exec'))
34
+ bump('high', 'can execute shell commands or code');
35
+ const roleComplete = (f) => f.untrustedInput.length > 0 && f.sensitiveSource.length > 0 && f.externalSink.length > 0;
36
+ const selfContained = flows.some((f) => f.selfContained && roleComplete(f));
37
+ const crossTrifecta = flows.some((f) => !f.selfContained && roleComplete(f));
38
+ if (crossTrifecta)
39
+ bump('high', 'untrusted-input, sensitive-source and egress co-exist across tools (toxic-flow surface)');
40
+ if (selfContained)
41
+ bump('critical', 'a single tool completes the exfiltration trifecta by itself');
42
+ if (has('code-exec') && has('untrusted-input'))
43
+ bump('critical', 'untrusted input can reach code execution');
44
+ // De-duplicate reasons while preserving order.
45
+ const seen = new Set();
46
+ const uniqueReasons = reasons.filter((r) => (seen.has(r) ? false : (seen.add(r), true)));
47
+ return { level, reasons: uniqueReasons, tags: [...tagSet] };
48
+ }
@@ -0,0 +1,10 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Deterministic scorer. Turns a set of findings into an auditable Trust Score:
4
+ * additive penalties from 100, diminishing returns per rule, per-category caps,
5
+ * then weakest-link hard gates. Same methodology version + same findings ⇒
6
+ * identical score, and every point is reconstructable from `vector`.
7
+ */
8
+ import type { Finding, Score } from '../types.js';
9
+ /** Compute the Trust Score for a set of findings (capability rules excluded). */
10
+ export declare function computeScore(findings: Finding[]): Score;