verification-layer 0.20.0 โ 0.22.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.
- package/README.md +251 -615
- package/dist/cli.js +542 -0
- package/dist/cli.js.map +1 -1
- package/dist/marketplace/index.d.ts +8 -0
- package/dist/marketplace/index.d.ts.map +1 -0
- package/dist/marketplace/index.js +7 -0
- package/dist/marketplace/index.js.map +1 -0
- package/dist/marketplace/installer.d.ts +62 -0
- package/dist/marketplace/installer.d.ts.map +1 -0
- package/dist/marketplace/installer.js +254 -0
- package/dist/marketplace/installer.js.map +1 -0
- package/dist/marketplace/registry.d.ts +52 -0
- package/dist/marketplace/registry.d.ts.map +1 -0
- package/dist/marketplace/registry.js +759 -0
- package/dist/marketplace/registry.js.map +1 -0
- package/dist/marketplace/types.d.ts +123 -0
- package/dist/marketplace/types.d.ts.map +1 -0
- package/dist/marketplace/types.js +6 -0
- package/dist/marketplace/types.js.map +1 -0
- package/dist/reporters/audit-report.d.ts.map +1 -1
- package/dist/reporters/audit-report.js +180 -0
- package/dist/reporters/audit-report.js.map +1 -1
- package/dist/reporters/index.d.ts.map +1 -1
- package/dist/reporters/index.js +2612 -5
- package/dist/reporters/index.js.map +1 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +15 -1
- package/dist/scan.js.map +1 -1
- package/dist/scanners/api-security/index.d.ts +7 -0
- package/dist/scanners/api-security/index.d.ts.map +1 -0
- package/dist/scanners/api-security/index.js +139 -0
- package/dist/scanners/api-security/index.js.map +1 -0
- package/dist/scanners/api-security/index.test.d.ts +5 -0
- package/dist/scanners/api-security/index.test.d.ts.map +1 -0
- package/dist/scanners/api-security/index.test.js +360 -0
- package/dist/scanners/api-security/index.test.js.map +1 -0
- package/dist/scanners/api-security/patterns.d.ts +32 -0
- package/dist/scanners/api-security/patterns.d.ts.map +1 -0
- package/dist/scanners/api-security/patterns.js +159 -0
- package/dist/scanners/api-security/patterns.js.map +1 -0
- package/dist/scanners/authentication/index.d.ts +7 -0
- package/dist/scanners/authentication/index.d.ts.map +1 -0
- package/dist/scanners/authentication/index.js +107 -0
- package/dist/scanners/authentication/index.js.map +1 -0
- package/dist/scanners/authentication/index.test.d.ts +5 -0
- package/dist/scanners/authentication/index.test.d.ts.map +1 -0
- package/dist/scanners/authentication/index.test.js +379 -0
- package/dist/scanners/authentication/index.test.js.map +1 -0
- package/dist/scanners/authentication/patterns.d.ts +32 -0
- package/dist/scanners/authentication/patterns.d.ts.map +1 -0
- package/dist/scanners/authentication/patterns.js +133 -0
- package/dist/scanners/authentication/patterns.js.map +1 -0
- package/dist/scanners/configuration/index.d.ts +8 -0
- package/dist/scanners/configuration/index.d.ts.map +1 -0
- package/dist/scanners/configuration/index.js +87 -0
- package/dist/scanners/configuration/index.js.map +1 -0
- package/dist/scanners/configuration/index.test.d.ts +5 -0
- package/dist/scanners/configuration/index.test.d.ts.map +1 -0
- package/dist/scanners/configuration/index.test.js +344 -0
- package/dist/scanners/configuration/index.test.js.map +1 -0
- package/dist/scanners/configuration/patterns.d.ts +32 -0
- package/dist/scanners/configuration/patterns.d.ts.map +1 -0
- package/dist/scanners/configuration/patterns.js +146 -0
- package/dist/scanners/configuration/patterns.js.map +1 -0
- package/dist/scanners/credentials/index.d.ts +7 -0
- package/dist/scanners/credentials/index.d.ts.map +1 -0
- package/dist/scanners/credentials/index.js +129 -0
- package/dist/scanners/credentials/index.js.map +1 -0
- package/dist/scanners/credentials/index.test.d.ts +5 -0
- package/dist/scanners/credentials/index.test.d.ts.map +1 -0
- package/dist/scanners/credentials/index.test.js +395 -0
- package/dist/scanners/credentials/index.test.js.map +1 -0
- package/dist/scanners/credentials/patterns.d.ts +32 -0
- package/dist/scanners/credentials/patterns.d.ts.map +1 -0
- package/dist/scanners/credentials/patterns.js +140 -0
- package/dist/scanners/credentials/patterns.js.map +1 -0
- package/dist/scanners/errors/index.d.ts +8 -0
- package/dist/scanners/errors/index.d.ts.map +1 -0
- package/dist/scanners/errors/index.js +78 -0
- package/dist/scanners/errors/index.js.map +1 -0
- package/dist/scanners/errors/index.test.d.ts +5 -0
- package/dist/scanners/errors/index.test.d.ts.map +1 -0
- package/dist/scanners/errors/index.test.js +330 -0
- package/dist/scanners/errors/index.test.js.map +1 -0
- package/dist/scanners/errors/patterns.d.ts +27 -0
- package/dist/scanners/errors/patterns.d.ts.map +1 -0
- package/dist/scanners/errors/patterns.js +97 -0
- package/dist/scanners/errors/patterns.js.map +1 -0
- package/dist/scanners/hipaa2026/index.d.ts +8 -0
- package/dist/scanners/hipaa2026/index.d.ts.map +1 -0
- package/dist/scanners/hipaa2026/index.js +345 -0
- package/dist/scanners/hipaa2026/index.js.map +1 -0
- package/dist/scanners/hipaa2026/index.test.d.ts +5 -0
- package/dist/scanners/hipaa2026/index.test.d.ts.map +1 -0
- package/dist/scanners/hipaa2026/index.test.js +332 -0
- package/dist/scanners/hipaa2026/index.test.js.map +1 -0
- package/dist/scanners/hipaa2026/patterns.d.ts +57 -0
- package/dist/scanners/hipaa2026/patterns.d.ts.map +1 -0
- package/dist/scanners/hipaa2026/patterns.js +268 -0
- package/dist/scanners/hipaa2026/patterns.js.map +1 -0
- package/dist/scanners/operational/index.d.ts +7 -0
- package/dist/scanners/operational/index.d.ts.map +1 -0
- package/dist/scanners/operational/index.js +171 -0
- package/dist/scanners/operational/index.js.map +1 -0
- package/dist/scanners/operational/index.test.d.ts +5 -0
- package/dist/scanners/operational/index.test.d.ts.map +1 -0
- package/dist/scanners/operational/index.test.js +406 -0
- package/dist/scanners/operational/index.test.js.map +1 -0
- package/dist/scanners/operational/patterns.d.ts +33 -0
- package/dist/scanners/operational/patterns.d.ts.map +1 -0
- package/dist/scanners/operational/patterns.js +151 -0
- package/dist/scanners/operational/patterns.js.map +1 -0
- package/dist/scanners/rbac/index.d.ts +7 -0
- package/dist/scanners/rbac/index.d.ts.map +1 -0
- package/dist/scanners/rbac/index.js +145 -0
- package/dist/scanners/rbac/index.js.map +1 -0
- package/dist/scanners/rbac/index.test.d.ts +5 -0
- package/dist/scanners/rbac/index.test.d.ts.map +1 -0
- package/dist/scanners/rbac/index.test.js +422 -0
- package/dist/scanners/rbac/index.test.js.map +1 -0
- package/dist/scanners/rbac/patterns.d.ts +32 -0
- package/dist/scanners/rbac/patterns.d.ts.map +1 -0
- package/dist/scanners/rbac/patterns.js +124 -0
- package/dist/scanners/rbac/patterns.js.map +1 -0
- package/dist/scanners/revocation/index.d.ts +8 -0
- package/dist/scanners/revocation/index.d.ts.map +1 -0
- package/dist/scanners/revocation/index.js +83 -0
- package/dist/scanners/revocation/index.js.map +1 -0
- package/dist/scanners/revocation/index.test.d.ts +5 -0
- package/dist/scanners/revocation/index.test.d.ts.map +1 -0
- package/dist/scanners/revocation/index.test.js +332 -0
- package/dist/scanners/revocation/index.test.js.map +1 -0
- package/dist/scanners/revocation/patterns.d.ts +27 -0
- package/dist/scanners/revocation/patterns.d.ts.map +1 -0
- package/dist/scanners/revocation/patterns.js +109 -0
- package/dist/scanners/revocation/patterns.js.map +1 -0
- package/dist/scanners/sanitization/index.d.ts +8 -0
- package/dist/scanners/sanitization/index.d.ts.map +1 -0
- package/dist/scanners/sanitization/index.js +98 -0
- package/dist/scanners/sanitization/index.js.map +1 -0
- package/dist/scanners/sanitization/index.test.d.ts +5 -0
- package/dist/scanners/sanitization/index.test.d.ts.map +1 -0
- package/dist/scanners/sanitization/index.test.js +370 -0
- package/dist/scanners/sanitization/index.test.js.map +1 -0
- package/dist/scanners/sanitization/patterns.d.ts +27 -0
- package/dist/scanners/sanitization/patterns.d.ts.map +1 -0
- package/dist/scanners/sanitization/patterns.js +117 -0
- package/dist/scanners/sanitization/patterns.js.map +1 -0
- package/dist/training/certificate.d.ts +26 -0
- package/dist/training/certificate.d.ts.map +1 -0
- package/dist/training/certificate.js +92 -0
- package/dist/training/certificate.js.map +1 -0
- package/dist/training/index.d.ts +3 -0
- package/dist/training/index.d.ts.map +1 -0
- package/dist/training/index.js +243 -0
- package/dist/training/index.js.map +1 -0
- package/dist/training/modules.d.ts +13 -0
- package/dist/training/modules.d.ts.map +1 -0
- package/dist/training/modules.js +608 -0
- package/dist/training/modules.js.map +1 -0
- package/dist/training/questions.d.ts +9 -0
- package/dist/training/questions.d.ts.map +1 -0
- package/dist/training/questions.js +505 -0
- package/dist/training/questions.js.map +1 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/npm-audit.d.ts +6 -0
- package/dist/utils/npm-audit.d.ts.map +1 -0
- package/dist/utils/npm-audit.js +95 -0
- package/dist/utils/npm-audit.js.map +1 -0
- package/dist/utils/scan-history.d.ts +59 -0
- package/dist/utils/scan-history.d.ts.map +1 -0
- package/dist/utils/scan-history.js +170 -0
- package/dist/utils/scan-history.js.map +1 -0
- package/package.json +4 -1
- package/templates/baa-verification-letter.md +105 -0
- package/templates/irp.md +545 -0
- package/templates/notice-of-privacy-practices.md +491 -0
- package/templates/physical-safeguards-checklist.md +247 -0
- package/templates/security-officer-designation.md +237 -0
package/dist/cli.js
CHANGED
|
@@ -32,6 +32,7 @@ program
|
|
|
32
32
|
.option('--min-confidence <level>', 'Minimum confidence level (high, medium, low)', 'low')
|
|
33
33
|
.option('--fix', 'Automatically fix detected issues where possible')
|
|
34
34
|
.option('--no-ai', 'Disable AI-powered triage and analysis')
|
|
35
|
+
.option('--audit', 'Run npm audit and include dependency vulnerabilities in report')
|
|
35
36
|
.action(async (path, options) => {
|
|
36
37
|
const spinner = ora('Scanning repository...').start();
|
|
37
38
|
const absolutePath = resolve(path);
|
|
@@ -70,6 +71,21 @@ program
|
|
|
70
71
|
minConfidence: options.minConfidence,
|
|
71
72
|
});
|
|
72
73
|
spinner.succeed(`Scan complete. Found ${result.findings.length} issues.`);
|
|
74
|
+
// Run npm audit if --audit flag is provided
|
|
75
|
+
let vulnerabilities;
|
|
76
|
+
if (options.audit) {
|
|
77
|
+
const auditSpinner = ora('Running npm audit...').start();
|
|
78
|
+
const { runNpmAudit } = await import('./utils/npm-audit.js');
|
|
79
|
+
const auditResult = await runNpmAudit(absolutePath);
|
|
80
|
+
if (auditResult.error) {
|
|
81
|
+
auditSpinner.warn(`npm audit: ${auditResult.error}`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const vulnCount = auditResult.vulnerabilities.length;
|
|
85
|
+
auditSpinner.succeed(`npm audit complete. Found ${vulnCount} vulnerabilities.`);
|
|
86
|
+
vulnerabilities = auditResult.vulnerabilities;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
73
89
|
// Apply fixes if --fix flag is provided
|
|
74
90
|
if (options.fix) {
|
|
75
91
|
const fixSpinner = ora('Applying automatic fixes...').start();
|
|
@@ -84,11 +100,23 @@ program
|
|
|
84
100
|
console.log(chalk.yellow('\nRun `vlayer audit <path> --generate-report` to generate PDF audit report.'));
|
|
85
101
|
}
|
|
86
102
|
}
|
|
103
|
+
// Get previous scan history and create comparison
|
|
104
|
+
const { getMostRecentScan, compareScan, saveScanHistory } = await import('./utils/scan-history.js');
|
|
105
|
+
const previousScan = await getMostRecentScan(absolutePath);
|
|
106
|
+
const comparison = result.complianceScore
|
|
107
|
+
? compareScan(result.complianceScore.score, result.findings, previousScan)
|
|
108
|
+
: null;
|
|
87
109
|
const reportOptions = {
|
|
88
110
|
format: options.format,
|
|
89
111
|
outputPath: options.output,
|
|
112
|
+
vulnerabilities,
|
|
113
|
+
scanComparison: comparison,
|
|
90
114
|
};
|
|
91
115
|
await generateReport(result, path, reportOptions);
|
|
116
|
+
// Save current scan to history
|
|
117
|
+
if (result.complianceScore) {
|
|
118
|
+
await saveScanHistory(absolutePath, result.complianceScore.score, result.findings, result.scannedFiles);
|
|
119
|
+
}
|
|
92
120
|
// Print summary
|
|
93
121
|
const acknowledged = result.findings.filter(f => f.acknowledged && !f.acknowledgment?.expired).length;
|
|
94
122
|
const suppressed = result.findings.filter(f => f.suppressed).length;
|
|
@@ -118,6 +146,29 @@ program
|
|
|
118
146
|
if (high > 0) {
|
|
119
147
|
console.log(chalk.yellow(` High (new): ${high}`));
|
|
120
148
|
}
|
|
149
|
+
// Display vulnerability summary if audit was run
|
|
150
|
+
if (vulnerabilities && vulnerabilities.length > 0) {
|
|
151
|
+
const vulnCounts = {
|
|
152
|
+
critical: vulnerabilities.filter(v => v.severity === 'critical').length,
|
|
153
|
+
high: vulnerabilities.filter(v => v.severity === 'high').length,
|
|
154
|
+
moderate: vulnerabilities.filter(v => v.severity === 'moderate').length,
|
|
155
|
+
low: vulnerabilities.filter(v => v.severity === 'low').length,
|
|
156
|
+
};
|
|
157
|
+
console.log('\n' + chalk.bold('Dependency Vulnerabilities:'));
|
|
158
|
+
console.log(` Total: ${vulnerabilities.length}`);
|
|
159
|
+
if (vulnCounts.critical > 0) {
|
|
160
|
+
console.log(chalk.red(` Critical: ${vulnCounts.critical}`));
|
|
161
|
+
}
|
|
162
|
+
if (vulnCounts.high > 0) {
|
|
163
|
+
console.log(chalk.yellow(` High: ${vulnCounts.high}`));
|
|
164
|
+
}
|
|
165
|
+
if (vulnCounts.moderate > 0) {
|
|
166
|
+
console.log(chalk.hex('#ca8a04')(` Moderate: ${vulnCounts.moderate}`));
|
|
167
|
+
}
|
|
168
|
+
if (vulnCounts.low > 0) {
|
|
169
|
+
console.log(chalk.blue(` Low: ${vulnCounts.low}`));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
121
172
|
// Exit with error code if new critical issues found (only if not fixing)
|
|
122
173
|
if (critical > 0 && !options.fix) {
|
|
123
174
|
process.exit(1);
|
|
@@ -876,5 +927,496 @@ rulesCommand
|
|
|
876
927
|
process.exit(1);
|
|
877
928
|
}
|
|
878
929
|
});
|
|
930
|
+
// Marketplace commands
|
|
931
|
+
const marketplaceCommand = program
|
|
932
|
+
.command('marketplace')
|
|
933
|
+
.alias('market')
|
|
934
|
+
.description('Community Rules Marketplace - Share and discover healthcare compliance rules');
|
|
935
|
+
marketplaceCommand
|
|
936
|
+
.command('search')
|
|
937
|
+
.description('Search for rules in the marketplace')
|
|
938
|
+
.argument('[query]', 'Search query (rule name, description, tags)', '')
|
|
939
|
+
.option('--framework <framework>', 'Filter by compliance framework (hipaa, state-law, payer-specific)')
|
|
940
|
+
.option('--jurisdiction <state>', 'Filter by state jurisdiction (california, new-york, texas, etc.)')
|
|
941
|
+
.option('--payer <payer>', 'Filter by payer (medicare, medicaid, bcbs, etc.)')
|
|
942
|
+
.option('--tech <stack>', 'Filter by tech stack (fhir, hl7, nextjs, etc.)')
|
|
943
|
+
.option('--verified', 'Show only verified rules')
|
|
944
|
+
.option('--min-rating <rating>', 'Minimum rating (1-5)', '0')
|
|
945
|
+
.option('--limit <number>', 'Maximum results to show', '20')
|
|
946
|
+
.action(async (query, options) => {
|
|
947
|
+
const spinner = ora('Searching marketplace...').start();
|
|
948
|
+
try {
|
|
949
|
+
const { MarketplaceRegistry } = await import('./marketplace/index.js');
|
|
950
|
+
const registry = new MarketplaceRegistry();
|
|
951
|
+
const filters = {};
|
|
952
|
+
if (options.framework)
|
|
953
|
+
filters.framework = options.framework;
|
|
954
|
+
if (options.jurisdiction)
|
|
955
|
+
filters.jurisdiction = options.jurisdiction;
|
|
956
|
+
if (options.payer)
|
|
957
|
+
filters.payer = options.payer;
|
|
958
|
+
if (options.tech)
|
|
959
|
+
filters.techStack = options.tech;
|
|
960
|
+
if (options.verified)
|
|
961
|
+
filters.verified = true;
|
|
962
|
+
if (options.minRating)
|
|
963
|
+
filters.minRating = parseFloat(options.minRating);
|
|
964
|
+
const result = await registry.search(query, filters, 1, parseInt(options.limit));
|
|
965
|
+
spinner.succeed(`Found ${result.total} rule(s)`);
|
|
966
|
+
if (result.rules.length === 0) {
|
|
967
|
+
console.log(chalk.yellow('\nNo rules found matching your criteria.'));
|
|
968
|
+
console.log(chalk.gray('Try broadening your search or removing filters.'));
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
console.log(chalk.bold('\n๐ฆ Marketplace Rules:\n'));
|
|
972
|
+
for (const rule of result.rules) {
|
|
973
|
+
const verified = rule.verified ? chalk.green('โ Verified') : chalk.gray('Unverified');
|
|
974
|
+
const rating = 'โญ'.repeat(Math.round(rule.rating));
|
|
975
|
+
console.log(chalk.cyan.bold(`${rule.id}`));
|
|
976
|
+
console.log(` ${rule.name} ${verified}`);
|
|
977
|
+
console.log(chalk.gray(` ${rule.description}`));
|
|
978
|
+
console.log(` ${rating} ${rule.rating.toFixed(1)} (${rule.reviews} reviews) | ${rule.downloads} downloads`);
|
|
979
|
+
console.log(chalk.gray(` Author: ${rule.author.name}${rule.author.organization ? ` (${rule.author.organization})` : ''}`));
|
|
980
|
+
console.log(chalk.gray(` Framework: ${rule.framework} | Severity: ${rule.severity} | Version: ${rule.version}`));
|
|
981
|
+
if (rule.jurisdiction) {
|
|
982
|
+
console.log(chalk.gray(` Jurisdiction: ${rule.jurisdiction}`));
|
|
983
|
+
}
|
|
984
|
+
if (rule.payer) {
|
|
985
|
+
console.log(chalk.gray(` Payer: ${rule.payer}`));
|
|
986
|
+
}
|
|
987
|
+
console.log(chalk.gray(` Tags: ${rule.tags.join(', ')}`));
|
|
988
|
+
console.log('');
|
|
989
|
+
}
|
|
990
|
+
console.log(chalk.gray(`\nShowing ${result.rules.length} of ${result.total} results`));
|
|
991
|
+
console.log(chalk.cyan('\nInstall a rule:') + chalk.white(' vlayer marketplace install <rule-id>'));
|
|
992
|
+
}
|
|
993
|
+
catch (error) {
|
|
994
|
+
spinner.fail('Search failed');
|
|
995
|
+
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
996
|
+
process.exit(1);
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
marketplaceCommand
|
|
1000
|
+
.command('install')
|
|
1001
|
+
.description('Install a rule from the marketplace')
|
|
1002
|
+
.argument('<rule-id>', 'Rule ID to install')
|
|
1003
|
+
.option('--version <version>', 'Specific version to install')
|
|
1004
|
+
.option('--package', 'Install as a package (collection of rules)')
|
|
1005
|
+
.action(async (ruleId, options) => {
|
|
1006
|
+
const spinner = ora(`Installing ${ruleId}...`).start();
|
|
1007
|
+
try {
|
|
1008
|
+
const { RulesInstaller } = await import('./marketplace/index.js');
|
|
1009
|
+
const installer = new RulesInstaller(process.cwd());
|
|
1010
|
+
if (options.package) {
|
|
1011
|
+
const installed = await installer.installPackage(ruleId);
|
|
1012
|
+
spinner.succeed(`Installed package with ${installed.length} rule(s)`);
|
|
1013
|
+
console.log(chalk.green('\nโ Package installed successfully!'));
|
|
1014
|
+
console.log(chalk.gray(`\nInstalled rules:`));
|
|
1015
|
+
installed.forEach(r => console.log(chalk.gray(` โข ${r.id} (v${r.version})`)));
|
|
1016
|
+
}
|
|
1017
|
+
else {
|
|
1018
|
+
const installed = await installer.install(ruleId, options.version);
|
|
1019
|
+
spinner.succeed('Rule installed');
|
|
1020
|
+
console.log(chalk.green('\nโ Rule installed successfully!'));
|
|
1021
|
+
console.log(chalk.gray(`\nRule: ${installed.id}`));
|
|
1022
|
+
console.log(chalk.gray(`Version: ${installed.version}`));
|
|
1023
|
+
console.log(chalk.gray(`Location: .vlayer/marketplace/${installed.id}.yaml`));
|
|
1024
|
+
}
|
|
1025
|
+
console.log(chalk.cyan('\nRun a scan:') + chalk.white(' vlayer scan .'));
|
|
1026
|
+
}
|
|
1027
|
+
catch (error) {
|
|
1028
|
+
spinner.fail('Installation failed');
|
|
1029
|
+
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
1030
|
+
process.exit(1);
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
marketplaceCommand
|
|
1034
|
+
.command('list')
|
|
1035
|
+
.description('List installed marketplace rules')
|
|
1036
|
+
.option('--enabled', 'Show only enabled rules')
|
|
1037
|
+
.option('--disabled', 'Show only disabled rules')
|
|
1038
|
+
.action(async (options) => {
|
|
1039
|
+
try {
|
|
1040
|
+
const { RulesInstaller } = await import('./marketplace/index.js');
|
|
1041
|
+
const installer = new RulesInstaller(process.cwd());
|
|
1042
|
+
let installed = await installer.listInstalled();
|
|
1043
|
+
if (options.enabled) {
|
|
1044
|
+
installed = installed.filter(r => r.enabled);
|
|
1045
|
+
}
|
|
1046
|
+
else if (options.disabled) {
|
|
1047
|
+
installed = installed.filter(r => !r.enabled);
|
|
1048
|
+
}
|
|
1049
|
+
if (installed.length === 0) {
|
|
1050
|
+
console.log(chalk.yellow('No marketplace rules installed.'));
|
|
1051
|
+
console.log(chalk.gray('\nSearch for rules:') + chalk.white(' vlayer marketplace search'));
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
console.log(chalk.bold(`\n๐ฆ Installed Rules (${installed.length}):\n`));
|
|
1055
|
+
for (const rule of installed) {
|
|
1056
|
+
const status = rule.enabled ? chalk.green('โ Enabled') : chalk.gray('โ Disabled');
|
|
1057
|
+
const source = rule.source === 'marketplace' ? chalk.blue('[Marketplace]') : chalk.gray('[Local]');
|
|
1058
|
+
console.log(`${status} ${chalk.cyan(rule.id)} ${source}`);
|
|
1059
|
+
console.log(chalk.gray(` Version: ${rule.version} | Installed: ${new Date(rule.installedAt).toLocaleDateString()}`));
|
|
1060
|
+
console.log('');
|
|
1061
|
+
}
|
|
1062
|
+
console.log(chalk.gray('Update all:') + chalk.white(' vlayer marketplace update'));
|
|
1063
|
+
}
|
|
1064
|
+
catch (error) {
|
|
1065
|
+
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
1066
|
+
process.exit(1);
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
marketplaceCommand
|
|
1070
|
+
.command('update')
|
|
1071
|
+
.description('Update installed rules to latest versions')
|
|
1072
|
+
.option('--dry-run', 'Show what would be updated without installing')
|
|
1073
|
+
.action(async (options) => {
|
|
1074
|
+
const spinner = ora('Checking for updates...').start();
|
|
1075
|
+
try {
|
|
1076
|
+
const { RulesInstaller } = await import('./marketplace/index.js');
|
|
1077
|
+
const installer = new RulesInstaller(process.cwd());
|
|
1078
|
+
const result = await installer.updateAll();
|
|
1079
|
+
spinner.succeed('Update check complete');
|
|
1080
|
+
if (result.updated.length === 0 && result.failed.length === 0) {
|
|
1081
|
+
console.log(chalk.green('\nโ All rules are up to date!'));
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (result.updated.length > 0) {
|
|
1085
|
+
console.log(chalk.green(`\nโ Updated ${result.updated.length} rule(s):`));
|
|
1086
|
+
result.updated.forEach(id => console.log(chalk.gray(` โข ${id}`)));
|
|
1087
|
+
}
|
|
1088
|
+
if (result.failed.length > 0) {
|
|
1089
|
+
console.log(chalk.red(`\nโ Failed to update ${result.failed.length} rule(s):`));
|
|
1090
|
+
result.failed.forEach(id => console.log(chalk.gray(` โข ${id}`)));
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
catch (error) {
|
|
1094
|
+
spinner.fail('Update failed');
|
|
1095
|
+
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
1096
|
+
process.exit(1);
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
marketplaceCommand
|
|
1100
|
+
.command('uninstall')
|
|
1101
|
+
.description('Uninstall a marketplace rule')
|
|
1102
|
+
.argument('<rule-id>', 'Rule ID to uninstall')
|
|
1103
|
+
.action(async (ruleId) => {
|
|
1104
|
+
const spinner = ora(`Uninstalling ${ruleId}...`).start();
|
|
1105
|
+
try {
|
|
1106
|
+
const { RulesInstaller } = await import('./marketplace/index.js');
|
|
1107
|
+
const installer = new RulesInstaller(process.cwd());
|
|
1108
|
+
await installer.uninstall(ruleId);
|
|
1109
|
+
spinner.succeed('Rule uninstalled');
|
|
1110
|
+
console.log(chalk.green(`\nโ ${ruleId} has been removed.`));
|
|
1111
|
+
}
|
|
1112
|
+
catch (error) {
|
|
1113
|
+
spinner.fail('Uninstall failed');
|
|
1114
|
+
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
1115
|
+
process.exit(1);
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
marketplaceCommand
|
|
1119
|
+
.command('featured')
|
|
1120
|
+
.description('Show featured/popular rules')
|
|
1121
|
+
.option('--limit <number>', 'Number of rules to show', '10')
|
|
1122
|
+
.action(async (options) => {
|
|
1123
|
+
const spinner = ora('Loading featured rules...').start();
|
|
1124
|
+
try {
|
|
1125
|
+
const { MarketplaceRegistry } = await import('./marketplace/index.js');
|
|
1126
|
+
const registry = new MarketplaceRegistry();
|
|
1127
|
+
const featured = await registry.getFeatured(parseInt(options.limit));
|
|
1128
|
+
spinner.succeed(`Found ${featured.length} featured rule(s)`);
|
|
1129
|
+
console.log(chalk.bold('\nโญ Featured Rules:\n'));
|
|
1130
|
+
for (const rule of featured) {
|
|
1131
|
+
const rating = 'โญ'.repeat(Math.round(rule.rating));
|
|
1132
|
+
console.log(chalk.cyan.bold(`${rule.id}`));
|
|
1133
|
+
console.log(` ${rule.name} ${chalk.green('โ')}`);
|
|
1134
|
+
console.log(chalk.gray(` ${rule.description}`));
|
|
1135
|
+
console.log(` ${rating} ${rule.rating.toFixed(1)} | ${rule.downloads} downloads`);
|
|
1136
|
+
console.log(chalk.gray(` ${rule.framework} โข ${rule.category} โข ${rule.severity}`));
|
|
1137
|
+
console.log('');
|
|
1138
|
+
}
|
|
1139
|
+
console.log(chalk.cyan('\nInstall:') + chalk.white(' vlayer marketplace install <rule-id>'));
|
|
1140
|
+
}
|
|
1141
|
+
catch (error) {
|
|
1142
|
+
spinner.fail('Failed to load featured rules');
|
|
1143
|
+
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
1144
|
+
process.exit(1);
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
marketplaceCommand
|
|
1148
|
+
.command('stats')
|
|
1149
|
+
.description('Show marketplace statistics')
|
|
1150
|
+
.action(async () => {
|
|
1151
|
+
const spinner = ora('Loading marketplace stats...').start();
|
|
1152
|
+
try {
|
|
1153
|
+
const { MarketplaceRegistry } = await import('./marketplace/index.js');
|
|
1154
|
+
const registry = new MarketplaceRegistry();
|
|
1155
|
+
const metadata = await registry.getMetadata();
|
|
1156
|
+
spinner.succeed('Stats loaded');
|
|
1157
|
+
console.log(chalk.bold('\n๐ Marketplace Statistics:\n'));
|
|
1158
|
+
console.log(`Total Rules: ${chalk.cyan(metadata.totalRules)}`);
|
|
1159
|
+
console.log(`Total Packages: ${chalk.cyan(metadata.totalPackages)}`);
|
|
1160
|
+
console.log('');
|
|
1161
|
+
console.log(chalk.bold('Rules by Framework:'));
|
|
1162
|
+
Object.entries(metadata.frameworks)
|
|
1163
|
+
.sort(([, a], [, b]) => b - a)
|
|
1164
|
+
.forEach(([framework, count]) => {
|
|
1165
|
+
console.log(` ${framework}: ${chalk.cyan(count)}`);
|
|
1166
|
+
});
|
|
1167
|
+
console.log('');
|
|
1168
|
+
console.log(chalk.bold('Rules by Category:'));
|
|
1169
|
+
Object.entries(metadata.categories)
|
|
1170
|
+
.sort(([, a], [, b]) => b - a)
|
|
1171
|
+
.forEach(([category, count]) => {
|
|
1172
|
+
console.log(` ${category}: ${chalk.cyan(count)}`);
|
|
1173
|
+
});
|
|
1174
|
+
console.log('');
|
|
1175
|
+
console.log(chalk.bold('Top Contributors:'));
|
|
1176
|
+
metadata.topContributors.slice(0, 5).forEach((contributor, i) => {
|
|
1177
|
+
console.log(` ${i + 1}. ${contributor.name}`);
|
|
1178
|
+
console.log(chalk.gray(` ${contributor.rulesPublished} rules | ${contributor.downloads} downloads`));
|
|
1179
|
+
});
|
|
1180
|
+
console.log('');
|
|
1181
|
+
console.log(chalk.gray('Search rules:') + chalk.white(' vlayer marketplace search'));
|
|
1182
|
+
}
|
|
1183
|
+
catch (error) {
|
|
1184
|
+
spinner.fail('Failed to load stats');
|
|
1185
|
+
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
1186
|
+
process.exit(1);
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
// Templates commands
|
|
1190
|
+
const templatesCommand = program
|
|
1191
|
+
.command('templates')
|
|
1192
|
+
.description('Export compliance document templates');
|
|
1193
|
+
templatesCommand
|
|
1194
|
+
.command('export')
|
|
1195
|
+
.description('Export a compliance document template')
|
|
1196
|
+
.argument('<template>', 'Template name: baa, physical, irp, npp, security-officer')
|
|
1197
|
+
.option('-o, --output <path>', 'Output file path')
|
|
1198
|
+
.action(async (template, options) => {
|
|
1199
|
+
try {
|
|
1200
|
+
const validTemplates = ['baa', 'physical', 'irp', 'npp', 'security-officer'];
|
|
1201
|
+
if (!validTemplates.includes(template)) {
|
|
1202
|
+
console.error(chalk.red(`Unknown template: ${template}`));
|
|
1203
|
+
console.log(chalk.yellow('\nAvailable templates:'));
|
|
1204
|
+
console.log(chalk.gray(' โข baa - Business Associate Agreement Verification Letter'));
|
|
1205
|
+
console.log(chalk.gray(' โข physical - Physical Safeguards Checklist'));
|
|
1206
|
+
console.log(chalk.gray(' โข irp - Incident Response Plan'));
|
|
1207
|
+
console.log(chalk.gray(' โข npp - Notice of Privacy Practices'));
|
|
1208
|
+
console.log(chalk.gray(' โข security-officer - Security Officer Designation'));
|
|
1209
|
+
process.exit(1);
|
|
1210
|
+
}
|
|
1211
|
+
// Read template file
|
|
1212
|
+
const { readFile: fsReadFile } = await import('fs/promises');
|
|
1213
|
+
const { fileURLToPath } = await import('url');
|
|
1214
|
+
const { dirname, join } = await import('path');
|
|
1215
|
+
// Get the directory of the current module
|
|
1216
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
1217
|
+
const __dirname = dirname(__filename);
|
|
1218
|
+
// Map template names to file names
|
|
1219
|
+
const templateFiles = {
|
|
1220
|
+
'baa': 'baa-verification-letter.md',
|
|
1221
|
+
'physical': 'physical-safeguards-checklist.md',
|
|
1222
|
+
'irp': 'irp.md',
|
|
1223
|
+
'npp': 'notice-of-privacy-practices.md',
|
|
1224
|
+
'security-officer': 'security-officer-designation.md',
|
|
1225
|
+
};
|
|
1226
|
+
const templateFileName = templateFiles[template];
|
|
1227
|
+
const templatePath = join(__dirname, '..', 'templates', templateFileName);
|
|
1228
|
+
const templateContent = await fsReadFile(templatePath, 'utf-8');
|
|
1229
|
+
// Determine output path
|
|
1230
|
+
const outputPath = options.output || `./${templateFileName}`;
|
|
1231
|
+
// Write template to output location
|
|
1232
|
+
await writeFile(outputPath, templateContent, 'utf-8');
|
|
1233
|
+
console.log(chalk.green(`\nโ Template exported successfully!`));
|
|
1234
|
+
console.log(chalk.cyan(`\nFile: ${outputPath}`));
|
|
1235
|
+
// Provide template-specific instructions
|
|
1236
|
+
if (template === 'baa') {
|
|
1237
|
+
console.log(chalk.gray('\nNext steps:'));
|
|
1238
|
+
console.log(chalk.gray(' 1. Open the file and fill in the bracketed placeholders:'));
|
|
1239
|
+
console.log(chalk.gray(' [BA COMPANY NAME], [DATE], [SCORE], etc.'));
|
|
1240
|
+
console.log(chalk.gray(' 2. Check all applicable boxes (โ โ โ)'));
|
|
1241
|
+
console.log(chalk.gray(' 3. Attach your vlayer compliance report as Exhibit A'));
|
|
1242
|
+
console.log(chalk.gray(' 4. Have both parties sign and date'));
|
|
1243
|
+
console.log(chalk.gray(' 5. Retain for 6 years per HIPAA requirements\n'));
|
|
1244
|
+
}
|
|
1245
|
+
else if (template === 'physical') {
|
|
1246
|
+
console.log(chalk.gray('\nNext steps:'));
|
|
1247
|
+
console.log(chalk.gray(' 1. Review Section A (Remote/Cloud) - applies to ALL organizations'));
|
|
1248
|
+
console.log(chalk.gray(' 2. Review Section B (Physical Office) - only if you have a physical location'));
|
|
1249
|
+
console.log(chalk.gray(' 3. Check all applicable boxes (โ โ โ)'));
|
|
1250
|
+
console.log(chalk.gray(' 4. Fill in verification details and dates'));
|
|
1251
|
+
console.log(chalk.gray(' 5. Complete remediation plan for non-compliant items'));
|
|
1252
|
+
console.log(chalk.gray(' 6. Sign and date the attestation'));
|
|
1253
|
+
console.log(chalk.gray(' 7. Retain for 6 years per HIPAA requirements\n'));
|
|
1254
|
+
}
|
|
1255
|
+
else if (template === 'irp') {
|
|
1256
|
+
console.log(chalk.gray('\nNext steps:'));
|
|
1257
|
+
console.log(chalk.gray(' 1. Fill in organization and IRT contact information'));
|
|
1258
|
+
console.log(chalk.gray(' 2. Customize severity levels and response times for your organization'));
|
|
1259
|
+
console.log(chalk.gray(' 3. Update external contact information (HHS, FBI, insurance, etc.)'));
|
|
1260
|
+
console.log(chalk.gray(' 4. Review and customize incident response procedures'));
|
|
1261
|
+
console.log(chalk.gray(' 5. Obtain executive approval and signatures'));
|
|
1262
|
+
console.log(chalk.gray(' 6. Distribute to all IRT members'));
|
|
1263
|
+
console.log(chalk.gray(' 7. Schedule quarterly drills and annual updates\n'));
|
|
1264
|
+
}
|
|
1265
|
+
else if (template === 'npp') {
|
|
1266
|
+
console.log(chalk.gray('\nNext steps:'));
|
|
1267
|
+
console.log(chalk.gray(' 1. Replace all [PLACEHOLDERS] with your organization\'s information'));
|
|
1268
|
+
console.log(chalk.gray(' 2. Review 2024 updates: 15-day timeline, in-person inspection rights'));
|
|
1269
|
+
console.log(chalk.gray(' 3. Customize uses and disclosures to match your practices'));
|
|
1270
|
+
console.log(chalk.gray(' 4. Review with healthcare attorney for state-specific requirements'));
|
|
1271
|
+
console.log(chalk.gray(' 5. Have Privacy Officer approve the final version'));
|
|
1272
|
+
console.log(chalk.gray(' 6. Post prominently in your facility and on website'));
|
|
1273
|
+
console.log(chalk.gray(' 7. Provide to all patients and obtain signed acknowledgment\n'));
|
|
1274
|
+
}
|
|
1275
|
+
else if (template === 'security-officer') {
|
|
1276
|
+
console.log(chalk.gray('\nNext steps:'));
|
|
1277
|
+
console.log(chalk.gray(' 1. Fill in Security Officer name and contact information'));
|
|
1278
|
+
console.log(chalk.gray(' 2. Customize responsibilities and authorities for your organization'));
|
|
1279
|
+
console.log(chalk.gray(' 3. Define budget, resources, and staff allocation'));
|
|
1280
|
+
console.log(chalk.gray(' 4. Identify backup/interim Security Officers'));
|
|
1281
|
+
console.log(chalk.gray(' 5. Obtain executive signature (CEO/COO)'));
|
|
1282
|
+
console.log(chalk.gray(' 6. Obtain Security Officer signature accepting designation'));
|
|
1283
|
+
console.log(chalk.gray(' 7. Distribute to stakeholders and HR\n'));
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
catch (error) {
|
|
1287
|
+
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
1288
|
+
process.exit(1);
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
templatesCommand
|
|
1292
|
+
.command('list')
|
|
1293
|
+
.description('List available templates')
|
|
1294
|
+
.action(() => {
|
|
1295
|
+
console.log(chalk.bold('\n๐ Available Compliance Templates:\n'));
|
|
1296
|
+
console.log(chalk.cyan('baa') + chalk.gray(' - Business Associate Agreement Verification Letter'));
|
|
1297
|
+
console.log(chalk.gray(' Annual security verification letter for HIPAA Business Associates'));
|
|
1298
|
+
console.log(chalk.gray(' Includes technical safeguards checklist and compliance report attachment'));
|
|
1299
|
+
console.log(chalk.gray(' Usage: vlayer templates export baa\n'));
|
|
1300
|
+
console.log(chalk.cyan('physical') + chalk.gray(' - Physical Safeguards Checklist'));
|
|
1301
|
+
console.log(chalk.gray(' HIPAA Physical Safeguards compliance checklist (ยง164.310)'));
|
|
1302
|
+
console.log(chalk.gray(' Section A: Remote/Cloud setup | Section B: Physical office controls'));
|
|
1303
|
+
console.log(chalk.gray(' Usage: vlayer templates export physical\n'));
|
|
1304
|
+
console.log(chalk.cyan('irp') + chalk.gray(' - Incident Response Plan'));
|
|
1305
|
+
console.log(chalk.gray(' Complete incident response procedures and breach notification guidelines'));
|
|
1306
|
+
console.log(chalk.gray(' Includes IRT roles, severity levels, 5-phase response, and HIPAA timeline'));
|
|
1307
|
+
console.log(chalk.gray(' Usage: vlayer templates export irp\n'));
|
|
1308
|
+
console.log(chalk.cyan('npp') + chalk.gray(' - Notice of Privacy Practices'));
|
|
1309
|
+
console.log(chalk.gray(' HIPAA ยง164.520 compliant patient notice (includes 2024 updates)'));
|
|
1310
|
+
console.log(chalk.gray(' 15-day access timeline, in-person inspection rights, electronic copy'));
|
|
1311
|
+
console.log(chalk.gray(' Usage: vlayer templates export npp\n'));
|
|
1312
|
+
console.log(chalk.cyan('security-officer') + chalk.gray(' - Security Officer Designation'));
|
|
1313
|
+
console.log(chalk.gray(' Official HIPAA ยง164.308(a)(2) required designation document'));
|
|
1314
|
+
console.log(chalk.gray(' Defines responsibilities, authorities, reporting structure, and signatures'));
|
|
1315
|
+
console.log(chalk.gray(' Usage: vlayer templates export security-officer\n'));
|
|
1316
|
+
});
|
|
1317
|
+
program
|
|
1318
|
+
.command('history')
|
|
1319
|
+
.description('Show scan history with compliance scores over time')
|
|
1320
|
+
.argument('[path]', 'Path to the project', '.')
|
|
1321
|
+
.option('-l, --limit <number>', 'Maximum number of scans to show', '10')
|
|
1322
|
+
.action(async (path, options) => {
|
|
1323
|
+
const absolutePath = resolve(path);
|
|
1324
|
+
try {
|
|
1325
|
+
const { getAllScans, formatHistoryDate } = await import('./utils/scan-history.js');
|
|
1326
|
+
const scans = await getAllScans(absolutePath);
|
|
1327
|
+
if (scans.length === 0) {
|
|
1328
|
+
console.log(chalk.yellow('\nNo scan history found.'));
|
|
1329
|
+
console.log(chalk.gray('Run `vlayer scan .` to create your first scan history entry.\n'));
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
const limit = parseInt(options.limit);
|
|
1333
|
+
const displayScans = scans.slice(0, limit);
|
|
1334
|
+
console.log(chalk.bold('\n๐ Scan History\n'));
|
|
1335
|
+
console.log(chalk.gray(`Showing ${displayScans.length} of ${scans.length} scan(s)\n`));
|
|
1336
|
+
// Find max and min scores for context
|
|
1337
|
+
const scores = scans.map(s => s.complianceScore);
|
|
1338
|
+
const maxScore = Math.max(...scores);
|
|
1339
|
+
const minScore = Math.min(...scores);
|
|
1340
|
+
for (let i = 0; i < displayScans.length; i++) {
|
|
1341
|
+
const scan = displayScans[i];
|
|
1342
|
+
const prevScan = i < scans.length - 1 ? scans[i + 1] : null;
|
|
1343
|
+
const scoreChange = prevScan ? scan.complianceScore - prevScan.complianceScore : 0;
|
|
1344
|
+
const scoreColor = scan.complianceScore >= 90 ? chalk.green
|
|
1345
|
+
: scan.complianceScore >= 70 ? chalk.yellow
|
|
1346
|
+
: chalk.red;
|
|
1347
|
+
const changeArrow = scoreChange > 0 ? chalk.green('โ')
|
|
1348
|
+
: scoreChange < 0 ? chalk.red('โ')
|
|
1349
|
+
: chalk.gray('โ');
|
|
1350
|
+
const changeStr = prevScan
|
|
1351
|
+
? ` (${scoreChange >= 0 ? '+' : ''}${scoreChange}) ${changeArrow}`
|
|
1352
|
+
: '';
|
|
1353
|
+
const date = formatHistoryDate(scan.date);
|
|
1354
|
+
const isBest = scan.complianceScore === maxScore;
|
|
1355
|
+
const isWorst = scan.complianceScore === minScore;
|
|
1356
|
+
console.log(chalk.cyan(date) +
|
|
1357
|
+
' ' +
|
|
1358
|
+
scoreColor.bold(`${scan.complianceScore}/100`) +
|
|
1359
|
+
changeStr +
|
|
1360
|
+
(isBest ? chalk.green.bold(' ๐ Best') : '') +
|
|
1361
|
+
(isWorst ? chalk.red(' โ ๏ธ Lowest') : ''));
|
|
1362
|
+
// Show severity breakdown
|
|
1363
|
+
const severityStr = [
|
|
1364
|
+
scan.severity.critical > 0 ? chalk.red(`${scan.severity.critical}C`) : null,
|
|
1365
|
+
scan.severity.high > 0 ? chalk.yellow(`${scan.severity.high}H`) : null,
|
|
1366
|
+
scan.severity.medium > 0 ? chalk.hex('#ca8a04')(`${scan.severity.medium}M`) : null,
|
|
1367
|
+
scan.severity.low > 0 ? chalk.blue(`${scan.severity.low}L`) : null,
|
|
1368
|
+
].filter(Boolean).join(' ');
|
|
1369
|
+
if (severityStr) {
|
|
1370
|
+
console.log(chalk.gray(` Issues: ${severityStr} โข Files: ${scan.totalFilesScanned}`));
|
|
1371
|
+
}
|
|
1372
|
+
else {
|
|
1373
|
+
console.log(chalk.gray(` No active issues โข Files: ${scan.totalFilesScanned}`));
|
|
1374
|
+
}
|
|
1375
|
+
console.log('');
|
|
1376
|
+
}
|
|
1377
|
+
if (scans.length > limit) {
|
|
1378
|
+
console.log(chalk.gray(`Use --limit ${scans.length} to see all scans\n`));
|
|
1379
|
+
}
|
|
1380
|
+
// Show statistics
|
|
1381
|
+
const avgScore = Math.round(scores.reduce((a, b) => a + b, 0) / scores.length);
|
|
1382
|
+
const trend = scans.length > 1
|
|
1383
|
+
? scans[0].complianceScore - scans[scans.length - 1].complianceScore
|
|
1384
|
+
: 0;
|
|
1385
|
+
console.log(chalk.bold('Statistics:'));
|
|
1386
|
+
console.log(` Total scans: ${scans.length}`);
|
|
1387
|
+
console.log(` Average score: ${avgScore}/100`);
|
|
1388
|
+
console.log(` Best score: ${chalk.green(maxScore)}/100`);
|
|
1389
|
+
console.log(` Lowest score: ${chalk.red(minScore)}/100`);
|
|
1390
|
+
if (trend !== 0) {
|
|
1391
|
+
const trendColor = trend > 0 ? chalk.green : chalk.red;
|
|
1392
|
+
const trendArrow = trend > 0 ? 'โ' : 'โ';
|
|
1393
|
+
console.log(` Overall trend: ${trendColor(`${trend >= 0 ? '+' : ''}${trend} ${trendArrow}`)}`);
|
|
1394
|
+
}
|
|
1395
|
+
console.log('');
|
|
1396
|
+
}
|
|
1397
|
+
catch (error) {
|
|
1398
|
+
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
1399
|
+
process.exit(1);
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
program
|
|
1403
|
+
.command('training')
|
|
1404
|
+
.description('Interactive HIPAA security training for developers')
|
|
1405
|
+
.option('--status', 'Show training completion status')
|
|
1406
|
+
.action(async (options) => {
|
|
1407
|
+
try {
|
|
1408
|
+
const { runTraining, showTrainingStatus } = await import('./training/index.js');
|
|
1409
|
+
if (options.status) {
|
|
1410
|
+
await showTrainingStatus();
|
|
1411
|
+
}
|
|
1412
|
+
else {
|
|
1413
|
+
await runTraining();
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
catch (error) {
|
|
1417
|
+
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
1418
|
+
process.exit(1);
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
879
1421
|
program.parse();
|
|
880
1422
|
//# sourceMappingURL=cli.js.map
|