licenseguard-cli 2.1.0 ā 2.2.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/CHANGELOG.md +285 -0
- package/CODE_OF_CONDUCT.md +43 -0
- package/CONTRIBUTING.md +68 -0
- package/README.md +48 -10
- package/bin/licenseguard.js +26 -0
- package/lib/commands/init-fast.js +1 -1
- package/lib/commands/init.js +7 -1
- package/lib/commands/scan.js +264 -5
- package/lib/formatters/html-generator.js +232 -0
- package/lib/scanner/color-mapper.js +87 -0
- package/lib/scanner/coverage-reporter.js +84 -0
- package/lib/scanner/deep-scan.js +138 -0
- package/lib/scanner/index.js +27 -4
- package/lib/scanner/license-detector.js +1 -1
- package/lib/scanner/path-tracer.js +81 -0
- package/lib/scanner/plugins/cpp.js +9 -1
- package/lib/scanner/plugins/go.js +9 -2
- package/lib/scanner/plugins/node.js +68 -34
- package/lib/scanner/plugins/python.js +93 -4
- package/lib/scanner/plugins/rust.js +9 -1
- package/lib/utils/update-notifier.js +141 -0
- package/package.json +1 -1
package/lib/commands/scan.js
CHANGED
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
const chalk = require('chalk')
|
|
7
7
|
const fs = require('fs')
|
|
8
8
|
const path = require('path')
|
|
9
|
+
const inquirer = require('inquirer')
|
|
10
|
+
const { runInit } = require('./init')
|
|
9
11
|
const { scanDependencies, displayConflictReport } = require('../scanner')
|
|
10
12
|
const { detectLicenseFromText } = require('../scanner/license-detector')
|
|
13
|
+
const { calculateCoverage, displayCoverage } = require('../scanner/coverage-reporter')
|
|
14
|
+
const { generateHTML, writeHTMLFile } = require('../formatters/html-generator')
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* Try to detect project license from local files
|
|
@@ -41,7 +45,9 @@ function detectProjectLicense() {
|
|
|
41
45
|
try {
|
|
42
46
|
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
|
43
47
|
if (pkg.license) return pkg.license
|
|
44
|
-
} catch (e) {
|
|
48
|
+
} catch (e) {
|
|
49
|
+
// Ignore malformed package.json
|
|
50
|
+
}
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
if (fs.existsSync('Cargo.toml')) {
|
|
@@ -51,6 +57,188 @@ function detectProjectLicense() {
|
|
|
51
57
|
return null
|
|
52
58
|
}
|
|
53
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Check if license string is valid (not null, empty, or UNLICENSED)
|
|
62
|
+
* @param {string|null} license - License to validate
|
|
63
|
+
* @returns {boolean} True if valid license
|
|
64
|
+
*/
|
|
65
|
+
function isValidLicense(license) {
|
|
66
|
+
return !!(license && license !== 'UNLICENSED' && license.trim() !== '')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Detect license from Node.js package.json
|
|
71
|
+
* @returns {string|null} Detected license or null
|
|
72
|
+
*/
|
|
73
|
+
function detectNodeLicense() {
|
|
74
|
+
try {
|
|
75
|
+
if (!fs.existsSync('package.json')) return null
|
|
76
|
+
|
|
77
|
+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
|
78
|
+
return pkg.license || null
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Detect license from Rust Cargo.toml
|
|
86
|
+
* @returns {string|null} Detected license or null
|
|
87
|
+
*/
|
|
88
|
+
function detectRustLicense() {
|
|
89
|
+
try {
|
|
90
|
+
if (!fs.existsSync('Cargo.toml')) return null
|
|
91
|
+
|
|
92
|
+
const content = fs.readFileSync('Cargo.toml', 'utf8')
|
|
93
|
+
const match = content.match(/license\s*=\s*"([^"]+)"/)
|
|
94
|
+
return match ? match[1] : null
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Detect license from Python pyproject.toml or setup.py
|
|
102
|
+
* @returns {string|null} Detected license or null
|
|
103
|
+
*/
|
|
104
|
+
function detectPythonLicense() {
|
|
105
|
+
// Try pyproject.toml first
|
|
106
|
+
try {
|
|
107
|
+
if (fs.existsSync('pyproject.toml')) {
|
|
108
|
+
const content = fs.readFileSync('pyproject.toml', 'utf8')
|
|
109
|
+
// Try different patterns for pyproject.toml
|
|
110
|
+
let match = content.match(/license\s*=\s*["{]([^"'}]+)["}]/)
|
|
111
|
+
if (!match) {
|
|
112
|
+
// Try project.license pattern
|
|
113
|
+
match = content.match(/project\.license\s*=\s*["{]([^"'}]+)["}]/)
|
|
114
|
+
}
|
|
115
|
+
if (match) return match[1]
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// Continue to setup.py fallback
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Fallback to setup.py
|
|
122
|
+
try {
|
|
123
|
+
if (fs.existsSync('setup.py')) {
|
|
124
|
+
const content = fs.readFileSync('setup.py', 'utf8')
|
|
125
|
+
const match = content.match(/license\s*=\s*["']([^"']+)["']/)
|
|
126
|
+
if (match) return match[1]
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
// Ignore
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Detect license from Go go.mod (less standardized)
|
|
137
|
+
* @returns {string|null} Detected license or null
|
|
138
|
+
*/
|
|
139
|
+
function detectGoLicense() {
|
|
140
|
+
try {
|
|
141
|
+
// Check go.mod for license comment
|
|
142
|
+
if (fs.existsSync('go.mod')) {
|
|
143
|
+
const content = fs.readFileSync('go.mod', 'utf8')
|
|
144
|
+
const match = content.match(/\/\/\s*license:\s*([^\n]+)/i)
|
|
145
|
+
if (match) return match[1].trim()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check common source files for license headers
|
|
149
|
+
const sourceFiles = ['main.go', 'LICENSE', 'LICENSE.txt']
|
|
150
|
+
for (const file of sourceFiles) {
|
|
151
|
+
if (fs.existsSync(file)) {
|
|
152
|
+
const content = fs.readFileSync(file, 'utf8')
|
|
153
|
+
// Look for SPDX-License-Identifier
|
|
154
|
+
const match = content.match(/SPDX-License-Identifier:\s*([^\n]+)/i)
|
|
155
|
+
if (match) return match[1].trim()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
// Ignore
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return null
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Detect license from C++ conanfile.txt or CMakeLists.txt
|
|
167
|
+
* @returns {string|null} Detected license or null
|
|
168
|
+
*/
|
|
169
|
+
function detectCppLicense() {
|
|
170
|
+
try {
|
|
171
|
+
// Try conanfile.txt
|
|
172
|
+
if (fs.existsSync('conanfile.txt')) {
|
|
173
|
+
const content = fs.readFileSync('conanfile.txt', 'utf8')
|
|
174
|
+
const match = content.match(/license\s*=\s*([^\n]+)/)
|
|
175
|
+
if (match) return match[1].trim()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Try CMakeLists.txt
|
|
179
|
+
if (fs.existsSync('CMakeLists.txt')) {
|
|
180
|
+
const content = fs.readFileSync('CMakeLists.txt', 'utf8')
|
|
181
|
+
// Look for license comment
|
|
182
|
+
const match = content.match(/#\s*license:\s*([^\n]+)/i)
|
|
183
|
+
if (match) return match[1].trim()
|
|
184
|
+
}
|
|
185
|
+
} catch (error) {
|
|
186
|
+
// Ignore
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Auto-detect project license from package manager files
|
|
194
|
+
* Tries multiple ecosystems in priority order
|
|
195
|
+
* @returns {string|null} Detected license or null
|
|
196
|
+
*/
|
|
197
|
+
function autoDetectLicense() {
|
|
198
|
+
const strategies = [
|
|
199
|
+
{ name: 'package.json', detector: detectNodeLicense },
|
|
200
|
+
{ name: 'Cargo.toml', detector: detectRustLicense },
|
|
201
|
+
{ name: 'pyproject.toml/setup.py', detector: detectPythonLicense },
|
|
202
|
+
{ name: 'go.mod', detector: detectGoLicense },
|
|
203
|
+
{ name: 'conanfile.txt/CMakeLists.txt', detector: detectCppLicense }
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
for (const strategy of strategies) {
|
|
207
|
+
const license = strategy.detector()
|
|
208
|
+
if (isValidLicense(license)) {
|
|
209
|
+
return license
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return null // Could not auto-detect
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Check if running in interactive mode (TTY and not CI)
|
|
218
|
+
* @returns {boolean} True if interactive mode
|
|
219
|
+
*/
|
|
220
|
+
function isInteractive() {
|
|
221
|
+
return process.stdin.isTTY && !process.env.CI
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get project name from package.json or current directory
|
|
226
|
+
* @returns {string} Project name
|
|
227
|
+
*/
|
|
228
|
+
function getProjectName() {
|
|
229
|
+
try {
|
|
230
|
+
const pkgPath = path.join(process.cwd(), 'package.json')
|
|
231
|
+
if (fs.existsSync(pkgPath)) {
|
|
232
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
233
|
+
return pkg.name || path.basename(process.cwd())
|
|
234
|
+
}
|
|
235
|
+
} catch (error) {
|
|
236
|
+
// Ignore errors, fall back to directory name
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return path.basename(process.cwd())
|
|
240
|
+
}
|
|
241
|
+
|
|
54
242
|
/**
|
|
55
243
|
* Run the scan command
|
|
56
244
|
* @param {Object} options - Command options
|
|
@@ -67,10 +255,59 @@ async function runScan(options) {
|
|
|
67
255
|
|
|
68
256
|
console.log(chalk.blue(`š Scanning in: ${process.cwd()}`))
|
|
69
257
|
|
|
70
|
-
// 2.
|
|
258
|
+
// 2. Check for config file existence
|
|
259
|
+
const configPath = '.licenseguardrc'
|
|
260
|
+
const configExists = fs.existsSync(configPath)
|
|
261
|
+
|
|
262
|
+
// 3. Determine Project License
|
|
71
263
|
let projectLicense = options.license
|
|
72
264
|
|
|
73
|
-
if (!projectLicense) {
|
|
265
|
+
if (!projectLicense && !configExists) {
|
|
266
|
+
// Config missing - determine mode (interactive vs CI/CD)
|
|
267
|
+
const interactive = isInteractive()
|
|
268
|
+
|
|
269
|
+
if (interactive) {
|
|
270
|
+
// Interactive mode: Prompt user to run init
|
|
271
|
+
console.log(chalk.yellow('ā ļø Configuration not found.\n'))
|
|
272
|
+
console.log('LicenseGuard needs to know your project\'s license to detect conflicts.')
|
|
273
|
+
console.log(chalk.gray('(e.g. MIT projects can\'t use GPL libraries)\n'))
|
|
274
|
+
|
|
275
|
+
const answer = await inquirer.prompt([{
|
|
276
|
+
type: 'confirm',
|
|
277
|
+
name: 'initNow',
|
|
278
|
+
message: 'Would you like to initialize configuration now?',
|
|
279
|
+
default: true
|
|
280
|
+
}])
|
|
281
|
+
|
|
282
|
+
if (answer.initNow) {
|
|
283
|
+
// Run init interactively
|
|
284
|
+
await runInit(options)
|
|
285
|
+
console.log(chalk.blue('\nš Continuing with scan...\n'))
|
|
286
|
+
// Config now exists, reload it
|
|
287
|
+
if (fs.existsSync(configPath)) {
|
|
288
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'))
|
|
289
|
+
projectLicense = config.license
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
console.log(chalk.yellow('Okay, scanning aborted. Run licenseguard init when ready.'))
|
|
293
|
+
process.exit(0)
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
// CI/CD mode: Auto-detect license
|
|
297
|
+
projectLicense = autoDetectLicense()
|
|
298
|
+
|
|
299
|
+
if (!projectLicense) {
|
|
300
|
+
console.error(chalk.red('ā Error: No configuration found and license could not be auto-detected.'))
|
|
301
|
+
console.log(chalk.yellow('\nFor CI/CD usage, commit .licenseguardrc to your repository:'))
|
|
302
|
+
console.log(chalk.blue(' 1. Run: licenseguard init'))
|
|
303
|
+
console.log(chalk.blue(' 2. Commit: git add .licenseguardrc && git commit\n'))
|
|
304
|
+
process.exit(1)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
console.log(chalk.blue(`ā¹ļø Auto-detected project license: ${projectLicense} (from package manager)\n`))
|
|
308
|
+
}
|
|
309
|
+
} else if (!projectLicense) {
|
|
310
|
+
// Config exists, try to load it
|
|
74
311
|
projectLicense = detectProjectLicense()
|
|
75
312
|
if (projectLicense) {
|
|
76
313
|
console.log(chalk.blue(`ā¹ļø Detected project license: ${projectLicense}`))
|
|
@@ -91,10 +328,21 @@ async function runScan(options) {
|
|
|
91
328
|
const results = await scanDependencies(projectLicense)
|
|
92
329
|
|
|
93
330
|
// 4. Display Report
|
|
94
|
-
const hasConflicts = displayConflictReport(results, projectLicense, {
|
|
331
|
+
const hasConflicts = await displayConflictReport(results, projectLicense, {
|
|
95
332
|
explain: options.explain
|
|
96
333
|
})
|
|
97
334
|
|
|
335
|
+
// 4.5. Display Coverage Report
|
|
336
|
+
const coverage = calculateCoverage(results)
|
|
337
|
+
displayCoverage(coverage)
|
|
338
|
+
|
|
339
|
+
// 4.6. Generate HTML Attribution (if requested)
|
|
340
|
+
if (options.format === 'html') {
|
|
341
|
+
const projectName = getProjectName()
|
|
342
|
+
const html = generateHTML(results, projectName)
|
|
343
|
+
writeHTMLFile(html, './CREDITS.html')
|
|
344
|
+
}
|
|
345
|
+
|
|
98
346
|
// 5. Handle Exit Code
|
|
99
347
|
if (hasConflicts && !options.allow) {
|
|
100
348
|
console.log(chalk.red('\nā Scan failed: License conflicts detected.'))
|
|
@@ -118,4 +366,15 @@ async function runScan(options) {
|
|
|
118
366
|
}
|
|
119
367
|
}
|
|
120
368
|
|
|
121
|
-
module.exports = {
|
|
369
|
+
module.exports = {
|
|
370
|
+
runScan,
|
|
371
|
+
autoDetectLicense,
|
|
372
|
+
detectNodeLicense,
|
|
373
|
+
detectRustLicense,
|
|
374
|
+
detectPythonLicense,
|
|
375
|
+
detectGoLicense,
|
|
376
|
+
detectCppLicense,
|
|
377
|
+
isValidLicense,
|
|
378
|
+
isInteractive,
|
|
379
|
+
getProjectName
|
|
380
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const chalk = require('chalk')
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generates mobile-optimized HTML attribution page
|
|
6
|
+
* @param {Object} scanResults - Scan results from scanner
|
|
7
|
+
* @param {Array} scanResults.packages - Array of package objects
|
|
8
|
+
* @param {string} projectName - Project name for title
|
|
9
|
+
* @returns {string} HTML content ready to write
|
|
10
|
+
* @example
|
|
11
|
+
* const html = generateHTML(scanResults, 'MyApp')
|
|
12
|
+
* fs.writeFileSync('CREDITS.html', html)
|
|
13
|
+
*/
|
|
14
|
+
function generateHTML(scanResults, projectName) {
|
|
15
|
+
const template = `<!DOCTYPE html>
|
|
16
|
+
<html lang="en">
|
|
17
|
+
<head>
|
|
18
|
+
<meta charset="UTF-8">
|
|
19
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
20
|
+
<title>Open Source Licenses - ${escapeHtml(projectName)}</title>
|
|
21
|
+
<style>
|
|
22
|
+
body {
|
|
23
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
24
|
+
line-height: 1.6;
|
|
25
|
+
max-width: 800px;
|
|
26
|
+
margin: 0 auto;
|
|
27
|
+
padding: 20px;
|
|
28
|
+
background: #f5f5f5;
|
|
29
|
+
color: #333;
|
|
30
|
+
}
|
|
31
|
+
h1 {
|
|
32
|
+
color: #333;
|
|
33
|
+
border-bottom: 2px solid #007AFF;
|
|
34
|
+
padding-bottom: 10px;
|
|
35
|
+
}
|
|
36
|
+
.package {
|
|
37
|
+
background: white;
|
|
38
|
+
padding: 15px;
|
|
39
|
+
margin: 10px 0;
|
|
40
|
+
border-radius: 8px;
|
|
41
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
42
|
+
}
|
|
43
|
+
.package-name {
|
|
44
|
+
font-weight: bold;
|
|
45
|
+
color: #007AFF;
|
|
46
|
+
font-size: 1.1em;
|
|
47
|
+
}
|
|
48
|
+
.license {
|
|
49
|
+
color: #666;
|
|
50
|
+
font-size: 0.9em;
|
|
51
|
+
margin-top: 5px;
|
|
52
|
+
}
|
|
53
|
+
.license-text {
|
|
54
|
+
background: #f9f9f9;
|
|
55
|
+
padding: 10px;
|
|
56
|
+
margin-top: 10px;
|
|
57
|
+
border-left: 3px solid #007AFF;
|
|
58
|
+
font-size: 0.85em;
|
|
59
|
+
white-space: pre-wrap;
|
|
60
|
+
display: none;
|
|
61
|
+
font-family: monospace;
|
|
62
|
+
}
|
|
63
|
+
.show-license {
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
color: #007AFF;
|
|
66
|
+
text-decoration: underline;
|
|
67
|
+
font-size: 0.9em;
|
|
68
|
+
margin-top: 10px;
|
|
69
|
+
display: inline-block;
|
|
70
|
+
min-height: 44px;
|
|
71
|
+
min-width: 44px;
|
|
72
|
+
padding: 12px;
|
|
73
|
+
line-height: 20px;
|
|
74
|
+
}
|
|
75
|
+
.footer {
|
|
76
|
+
text-align: center;
|
|
77
|
+
margin-top: 40px;
|
|
78
|
+
color: #999;
|
|
79
|
+
font-size: 0.85em;
|
|
80
|
+
}
|
|
81
|
+
.footer a {
|
|
82
|
+
color: #007AFF;
|
|
83
|
+
text-decoration: none;
|
|
84
|
+
}
|
|
85
|
+
.footer a:hover {
|
|
86
|
+
text-decoration: underline;
|
|
87
|
+
}
|
|
88
|
+
@media (max-width: 768px) {
|
|
89
|
+
body {
|
|
90
|
+
padding: 10px;
|
|
91
|
+
}
|
|
92
|
+
.package {
|
|
93
|
+
padding: 12px;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
@media (prefers-color-scheme: dark) {
|
|
97
|
+
body {
|
|
98
|
+
background: #1c1c1e;
|
|
99
|
+
color: #f5f5f5;
|
|
100
|
+
}
|
|
101
|
+
.package {
|
|
102
|
+
background: #2c2c2e;
|
|
103
|
+
}
|
|
104
|
+
.license-text {
|
|
105
|
+
background: #3a3a3c;
|
|
106
|
+
}
|
|
107
|
+
h1 {
|
|
108
|
+
color: #f5f5f5;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
</style>
|
|
112
|
+
</head>
|
|
113
|
+
<body>
|
|
114
|
+
<h1>Open Source Licenses</h1>
|
|
115
|
+
<p>This application uses the following open source libraries:</p>
|
|
116
|
+
|
|
117
|
+
${generatePackageCards(scanResults.packages)}
|
|
118
|
+
|
|
119
|
+
<div class="footer">
|
|
120
|
+
<p>Generated by <a href="https://www.npmjs.com/package/licenseguard-cli">LicenseGuard</a> v${getVersion()}</p>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<script>
|
|
124
|
+
// Progressive enhancement: show/hide license text
|
|
125
|
+
document.querySelectorAll('.show-license').forEach(link => {
|
|
126
|
+
link.addEventListener('click', function() {
|
|
127
|
+
const licenseText = this.nextElementSibling
|
|
128
|
+
if (licenseText.style.display === 'none' || !licenseText.style.display) {
|
|
129
|
+
licenseText.style.display = 'block'
|
|
130
|
+
this.textContent = 'Hide license text'
|
|
131
|
+
} else {
|
|
132
|
+
licenseText.style.display = 'none'
|
|
133
|
+
this.textContent = 'Show license text'
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
</script>
|
|
138
|
+
</body>
|
|
139
|
+
</html>`
|
|
140
|
+
|
|
141
|
+
return template
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Generates HTML package cards for all packages
|
|
146
|
+
* @param {Array} packages - Array of package objects
|
|
147
|
+
* @returns {string} HTML string with all package cards
|
|
148
|
+
*/
|
|
149
|
+
function generatePackageCards(packages) {
|
|
150
|
+
if (!packages || packages.length === 0) {
|
|
151
|
+
return '<div class="package"><p>No packages found</p></div>'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Sort alphabetically by package name
|
|
155
|
+
const sortedPackages = [...packages].sort((a, b) =>
|
|
156
|
+
a.name.localeCompare(b.name)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return sortedPackages.map(pkg => {
|
|
160
|
+
const licenseTextHtml = pkg.licenseText
|
|
161
|
+
? `
|
|
162
|
+
<div class="show-license">Show license text</div>
|
|
163
|
+
<div class="license-text">${escapeHtml(pkg.licenseText)}</div>
|
|
164
|
+
`
|
|
165
|
+
: ''
|
|
166
|
+
|
|
167
|
+
return `
|
|
168
|
+
<div class="package">
|
|
169
|
+
<div class="package-name">${escapeHtml(pkg.name)} v${escapeHtml(pkg.version)}</div>
|
|
170
|
+
<div class="license">License: ${escapeHtml(pkg.license)}</div>${licenseTextHtml}
|
|
171
|
+
</div>
|
|
172
|
+
`.trim()
|
|
173
|
+
}).join('\n')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Escapes HTML special characters to prevent XSS
|
|
178
|
+
* @param {string} text - Text to escape
|
|
179
|
+
* @returns {string} Escaped text safe for HTML
|
|
180
|
+
*/
|
|
181
|
+
function escapeHtml(text) {
|
|
182
|
+
if (!text) return ''
|
|
183
|
+
|
|
184
|
+
return String(text)
|
|
185
|
+
.replace(/&/g, '&')
|
|
186
|
+
.replace(/</g, '<')
|
|
187
|
+
.replace(/>/g, '>')
|
|
188
|
+
.replace(/"/g, '"')
|
|
189
|
+
.replace(/'/g, ''')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Gets LicenseGuard version from package.json
|
|
194
|
+
* @returns {string} Version string
|
|
195
|
+
*/
|
|
196
|
+
function getVersion() {
|
|
197
|
+
try {
|
|
198
|
+
const packageJson = require('../../package.json')
|
|
199
|
+
return packageJson.version
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return '2.2.0'
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Writes HTML file to disk with error handling
|
|
207
|
+
* @param {string} html - HTML content to write
|
|
208
|
+
* @param {string} outputPath - Path to write file
|
|
209
|
+
*/
|
|
210
|
+
function writeHTMLFile(html, outputPath) {
|
|
211
|
+
try {
|
|
212
|
+
fs.writeFileSync(outputPath, html, 'utf8')
|
|
213
|
+
console.log(chalk.green(`ā Attribution page saved to: ${outputPath}`))
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (error.code === 'EACCES') {
|
|
216
|
+
console.error(chalk.red('ā Error: Could not write CREDITS.html (permission denied)'))
|
|
217
|
+
console.log(chalk.yellow('Try running with elevated permissions or choose different output directory'))
|
|
218
|
+
} else if (error.code === 'ENOSPC') {
|
|
219
|
+
console.error(chalk.red('ā Error: Could not write CREDITS.html (disk full)'))
|
|
220
|
+
} else {
|
|
221
|
+
console.error(chalk.red(`ā Error: Could not write CREDITS.html (${error.message})`))
|
|
222
|
+
}
|
|
223
|
+
process.exit(1)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = {
|
|
228
|
+
generateHTML,
|
|
229
|
+
escapeHtml,
|
|
230
|
+
generatePackageCards,
|
|
231
|
+
writeHTMLFile
|
|
232
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const chalk = require('chalk')
|
|
2
|
+
|
|
3
|
+
// License color mapping based on safety level
|
|
4
|
+
const LICENSE_COLORS = {
|
|
5
|
+
// Green: Permissive licenses
|
|
6
|
+
'MIT': 'green',
|
|
7
|
+
'Apache-2.0': 'green',
|
|
8
|
+
'BSD-2-Clause': 'green',
|
|
9
|
+
'BSD-3-Clause': 'green',
|
|
10
|
+
'ISC': 'green',
|
|
11
|
+
'0BSD': 'green',
|
|
12
|
+
'Unlicense': 'green',
|
|
13
|
+
|
|
14
|
+
// Yellow: Weak copyleft
|
|
15
|
+
'MPL-2.0': 'yellow',
|
|
16
|
+
'LGPL-2.1': 'yellow',
|
|
17
|
+
'LGPL-3.0': 'yellow',
|
|
18
|
+
'EPL-1.0': 'yellow',
|
|
19
|
+
'EPL-2.0': 'yellow',
|
|
20
|
+
|
|
21
|
+
// Red: Strong copyleft
|
|
22
|
+
'GPL-2.0': 'red',
|
|
23
|
+
'GPL-3.0': 'red',
|
|
24
|
+
'AGPL-3.0': 'red',
|
|
25
|
+
|
|
26
|
+
// Gray: Unknown
|
|
27
|
+
'UNKNOWN': 'gray'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Emoji indicators for accessibility
|
|
31
|
+
const EMOJI_INDICATORS = {
|
|
32
|
+
green: 'š¢',
|
|
33
|
+
yellow: 'ā ļø',
|
|
34
|
+
red: 'ā',
|
|
35
|
+
gray: 'ā'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get color for a given license
|
|
40
|
+
* @param {string} license - License identifier (e.g., "MIT", "GPL-3.0")
|
|
41
|
+
* @returns {string} - Color name (green/yellow/red/gray)
|
|
42
|
+
*/
|
|
43
|
+
function getColor(license) {
|
|
44
|
+
return LICENSE_COLORS[license] || 'gray'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get emoji indicator for a given license
|
|
49
|
+
* @param {string} license - License identifier
|
|
50
|
+
* @returns {string} - Emoji indicator
|
|
51
|
+
*/
|
|
52
|
+
function getEmoji(license) {
|
|
53
|
+
const color = getColor(license)
|
|
54
|
+
return EMOJI_INDICATORS[color]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Colorize text based on license safety level
|
|
59
|
+
* @param {string} license - License identifier
|
|
60
|
+
* @param {string} text - Text to colorize
|
|
61
|
+
* @returns {string} - Colorized text with chalk
|
|
62
|
+
*/
|
|
63
|
+
function colorize(license, text) {
|
|
64
|
+
const color = getColor(license)
|
|
65
|
+
return chalk[color](text)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Colorize text with emoji indicator
|
|
70
|
+
* @param {string} license - License identifier
|
|
71
|
+
* @param {string} text - Text to colorize
|
|
72
|
+
* @returns {string} - Colorized text with emoji prefix
|
|
73
|
+
*/
|
|
74
|
+
function colorizeWithEmoji(license, text) {
|
|
75
|
+
const emoji = getEmoji(license)
|
|
76
|
+
const coloredText = colorize(license, text)
|
|
77
|
+
return `${emoji} ${coloredText}`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
getColor,
|
|
82
|
+
getEmoji,
|
|
83
|
+
colorize,
|
|
84
|
+
colorizeWithEmoji,
|
|
85
|
+
LICENSE_COLORS,
|
|
86
|
+
EMOJI_INDICATORS
|
|
87
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const chalk = require('chalk')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Calculate coverage statistics from scan results
|
|
5
|
+
* @param {Object} scanResults - Scan results object
|
|
6
|
+
* @returns {Object} Coverage statistics { total, identified, unknown, percentage }
|
|
7
|
+
*/
|
|
8
|
+
function calculateCoverage(scanResults) {
|
|
9
|
+
// Handle cases where scanResults might be undefined or null
|
|
10
|
+
if (!scanResults) {
|
|
11
|
+
return {
|
|
12
|
+
total: 0,
|
|
13
|
+
identified: 0,
|
|
14
|
+
unknown: 0,
|
|
15
|
+
percentage: 0.0,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Support both old format (packages array) and new format (totalDependencies)
|
|
20
|
+
let totalPackages = 0
|
|
21
|
+
let unknownPackages = 0
|
|
22
|
+
|
|
23
|
+
if (scanResults.packages) {
|
|
24
|
+
// Old format: packages array
|
|
25
|
+
totalPackages = scanResults.packages.length
|
|
26
|
+
unknownPackages = scanResults.packages.filter(p => p.license === 'UNKNOWN').length
|
|
27
|
+
} else if (scanResults.totalDependencies !== undefined) {
|
|
28
|
+
// New format: totalDependencies, compatible, incompatible, unknown
|
|
29
|
+
totalPackages = scanResults.totalDependencies
|
|
30
|
+
unknownPackages = scanResults.unknown || 0
|
|
31
|
+
} else {
|
|
32
|
+
// Unknown format
|
|
33
|
+
return {
|
|
34
|
+
total: 0,
|
|
35
|
+
identified: 0,
|
|
36
|
+
unknown: 0,
|
|
37
|
+
percentage: 0.0,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const identifiedPackages = totalPackages - unknownPackages
|
|
42
|
+
const percentage = totalPackages > 0
|
|
43
|
+
? ((identifiedPackages / totalPackages) * 100).toFixed(1)
|
|
44
|
+
: '0.0'
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
total: totalPackages,
|
|
48
|
+
identified: identifiedPackages,
|
|
49
|
+
unknown: unknownPackages,
|
|
50
|
+
percentage: parseFloat(percentage),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get emoji indicator based on coverage percentage
|
|
56
|
+
* @param {number} percentage - Coverage percentage
|
|
57
|
+
* @returns {string} Emoji indicator (ā
/ā ļø/ā)
|
|
58
|
+
*/
|
|
59
|
+
function getCoverageEmoji(percentage) {
|
|
60
|
+
if (percentage >= 90) return 'ā
'
|
|
61
|
+
if (percentage >= 70) return 'ā ļø'
|
|
62
|
+
return 'ā'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Display coverage report to console
|
|
67
|
+
* @param {Object} coverage - Coverage object from calculateCoverage
|
|
68
|
+
*/
|
|
69
|
+
function displayCoverage(coverage) {
|
|
70
|
+
console.log(chalk.blue('\nš Scan Coverage:'))
|
|
71
|
+
|
|
72
|
+
const emoji = getCoverageEmoji(coverage.percentage)
|
|
73
|
+
console.log(`${emoji} ${coverage.identified}/${coverage.total} packages identified (${coverage.percentage}%)`)
|
|
74
|
+
|
|
75
|
+
if (coverage.unknown > 0) {
|
|
76
|
+
console.log(chalk.yellow(`ā ļø ${coverage.unknown} packages with unknown licenses`))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (coverage.percentage < 80) {
|
|
80
|
+
console.log(chalk.yellow('\nš” Tip: Some packages may need manual LICENSE file inspection'))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { calculateCoverage, displayCoverage, getCoverageEmoji }
|