devvami 1.4.0 → 1.4.2
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/oclif.manifest.json +275 -89
- package/package.json +1 -1
- package/src/commands/vuln/detail.js +65 -0
- package/src/commands/vuln/scan.js +210 -0
- package/src/commands/vuln/search.js +128 -0
- package/src/formatters/vuln.js +317 -0
- package/src/help.js +15 -0
- package/src/services/audit-detector.js +120 -0
- package/src/services/audit-runner.js +365 -0
- package/src/services/nvd.js +245 -0
- package/src/types.js +9 -5
- package/src/utils/errors.js +2 -0
- package/src/utils/tui/modal.js +224 -0
- package/src/utils/tui/navigable-table.js +504 -0
package/oclif.manifest.json
CHANGED
|
@@ -520,94 +520,6 @@
|
|
|
520
520
|
"trend.js"
|
|
521
521
|
]
|
|
522
522
|
},
|
|
523
|
-
"create:repo": {
|
|
524
|
-
"aliases": [],
|
|
525
|
-
"args": {
|
|
526
|
-
"template": {
|
|
527
|
-
"description": "Nome del template",
|
|
528
|
-
"name": "template",
|
|
529
|
-
"required": false
|
|
530
|
-
}
|
|
531
|
-
},
|
|
532
|
-
"description": "Crea nuovo progetto da template GitHub o lista i template disponibili",
|
|
533
|
-
"examples": [
|
|
534
|
-
"<%= config.bin %> create repo --list",
|
|
535
|
-
"<%= config.bin %> create repo --list --search \"lambda\"",
|
|
536
|
-
"<%= config.bin %> create repo template-lambda",
|
|
537
|
-
"<%= config.bin %> create repo template-lambda --name my-service --dry-run"
|
|
538
|
-
],
|
|
539
|
-
"flags": {
|
|
540
|
-
"json": {
|
|
541
|
-
"description": "Format output as json.",
|
|
542
|
-
"helpGroup": "GLOBAL",
|
|
543
|
-
"name": "json",
|
|
544
|
-
"allowNo": false,
|
|
545
|
-
"type": "boolean"
|
|
546
|
-
},
|
|
547
|
-
"list": {
|
|
548
|
-
"description": "Lista template disponibili",
|
|
549
|
-
"name": "list",
|
|
550
|
-
"allowNo": false,
|
|
551
|
-
"type": "boolean"
|
|
552
|
-
},
|
|
553
|
-
"search": {
|
|
554
|
-
"char": "s",
|
|
555
|
-
"description": "Cerca in nome e descrizione dei template (case-insensitive)",
|
|
556
|
-
"name": "search",
|
|
557
|
-
"hasDynamicHelp": false,
|
|
558
|
-
"multiple": false,
|
|
559
|
-
"type": "option"
|
|
560
|
-
},
|
|
561
|
-
"name": {
|
|
562
|
-
"description": "Nome del nuovo repository",
|
|
563
|
-
"name": "name",
|
|
564
|
-
"hasDynamicHelp": false,
|
|
565
|
-
"multiple": false,
|
|
566
|
-
"type": "option"
|
|
567
|
-
},
|
|
568
|
-
"description": {
|
|
569
|
-
"description": "Descrizione del repository",
|
|
570
|
-
"name": "description",
|
|
571
|
-
"default": "",
|
|
572
|
-
"hasDynamicHelp": false,
|
|
573
|
-
"multiple": false,
|
|
574
|
-
"type": "option"
|
|
575
|
-
},
|
|
576
|
-
"private": {
|
|
577
|
-
"description": "Repository privato (default)",
|
|
578
|
-
"name": "private",
|
|
579
|
-
"allowNo": false,
|
|
580
|
-
"type": "boolean"
|
|
581
|
-
},
|
|
582
|
-
"public": {
|
|
583
|
-
"description": "Repository pubblico",
|
|
584
|
-
"name": "public",
|
|
585
|
-
"allowNo": false,
|
|
586
|
-
"type": "boolean"
|
|
587
|
-
},
|
|
588
|
-
"dry-run": {
|
|
589
|
-
"description": "Preview senza eseguire",
|
|
590
|
-
"name": "dry-run",
|
|
591
|
-
"allowNo": false,
|
|
592
|
-
"type": "boolean"
|
|
593
|
-
}
|
|
594
|
-
},
|
|
595
|
-
"hasDynamicHelp": false,
|
|
596
|
-
"hiddenAliases": [],
|
|
597
|
-
"id": "create:repo",
|
|
598
|
-
"pluginAlias": "devvami",
|
|
599
|
-
"pluginName": "devvami",
|
|
600
|
-
"pluginType": "core",
|
|
601
|
-
"strict": true,
|
|
602
|
-
"enableJsonFlag": true,
|
|
603
|
-
"isESM": true,
|
|
604
|
-
"relativePath": [
|
|
605
|
-
"src",
|
|
606
|
-
"commands",
|
|
607
|
-
"create",
|
|
608
|
-
"repo.js"
|
|
609
|
-
]
|
|
610
|
-
},
|
|
611
523
|
"docs:list": {
|
|
612
524
|
"aliases": [],
|
|
613
525
|
"args": {},
|
|
@@ -813,6 +725,94 @@
|
|
|
813
725
|
"search.js"
|
|
814
726
|
]
|
|
815
727
|
},
|
|
728
|
+
"create:repo": {
|
|
729
|
+
"aliases": [],
|
|
730
|
+
"args": {
|
|
731
|
+
"template": {
|
|
732
|
+
"description": "Nome del template",
|
|
733
|
+
"name": "template",
|
|
734
|
+
"required": false
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
"description": "Crea nuovo progetto da template GitHub o lista i template disponibili",
|
|
738
|
+
"examples": [
|
|
739
|
+
"<%= config.bin %> create repo --list",
|
|
740
|
+
"<%= config.bin %> create repo --list --search \"lambda\"",
|
|
741
|
+
"<%= config.bin %> create repo template-lambda",
|
|
742
|
+
"<%= config.bin %> create repo template-lambda --name my-service --dry-run"
|
|
743
|
+
],
|
|
744
|
+
"flags": {
|
|
745
|
+
"json": {
|
|
746
|
+
"description": "Format output as json.",
|
|
747
|
+
"helpGroup": "GLOBAL",
|
|
748
|
+
"name": "json",
|
|
749
|
+
"allowNo": false,
|
|
750
|
+
"type": "boolean"
|
|
751
|
+
},
|
|
752
|
+
"list": {
|
|
753
|
+
"description": "Lista template disponibili",
|
|
754
|
+
"name": "list",
|
|
755
|
+
"allowNo": false,
|
|
756
|
+
"type": "boolean"
|
|
757
|
+
},
|
|
758
|
+
"search": {
|
|
759
|
+
"char": "s",
|
|
760
|
+
"description": "Cerca in nome e descrizione dei template (case-insensitive)",
|
|
761
|
+
"name": "search",
|
|
762
|
+
"hasDynamicHelp": false,
|
|
763
|
+
"multiple": false,
|
|
764
|
+
"type": "option"
|
|
765
|
+
},
|
|
766
|
+
"name": {
|
|
767
|
+
"description": "Nome del nuovo repository",
|
|
768
|
+
"name": "name",
|
|
769
|
+
"hasDynamicHelp": false,
|
|
770
|
+
"multiple": false,
|
|
771
|
+
"type": "option"
|
|
772
|
+
},
|
|
773
|
+
"description": {
|
|
774
|
+
"description": "Descrizione del repository",
|
|
775
|
+
"name": "description",
|
|
776
|
+
"default": "",
|
|
777
|
+
"hasDynamicHelp": false,
|
|
778
|
+
"multiple": false,
|
|
779
|
+
"type": "option"
|
|
780
|
+
},
|
|
781
|
+
"private": {
|
|
782
|
+
"description": "Repository privato (default)",
|
|
783
|
+
"name": "private",
|
|
784
|
+
"allowNo": false,
|
|
785
|
+
"type": "boolean"
|
|
786
|
+
},
|
|
787
|
+
"public": {
|
|
788
|
+
"description": "Repository pubblico",
|
|
789
|
+
"name": "public",
|
|
790
|
+
"allowNo": false,
|
|
791
|
+
"type": "boolean"
|
|
792
|
+
},
|
|
793
|
+
"dry-run": {
|
|
794
|
+
"description": "Preview senza eseguire",
|
|
795
|
+
"name": "dry-run",
|
|
796
|
+
"allowNo": false,
|
|
797
|
+
"type": "boolean"
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
"hasDynamicHelp": false,
|
|
801
|
+
"hiddenAliases": [],
|
|
802
|
+
"id": "create:repo",
|
|
803
|
+
"pluginAlias": "devvami",
|
|
804
|
+
"pluginName": "devvami",
|
|
805
|
+
"pluginType": "core",
|
|
806
|
+
"strict": true,
|
|
807
|
+
"enableJsonFlag": true,
|
|
808
|
+
"isESM": true,
|
|
809
|
+
"relativePath": [
|
|
810
|
+
"src",
|
|
811
|
+
"commands",
|
|
812
|
+
"create",
|
|
813
|
+
"repo.js"
|
|
814
|
+
]
|
|
815
|
+
},
|
|
816
816
|
"dotfiles:add": {
|
|
817
817
|
"aliases": [],
|
|
818
818
|
"args": {
|
|
@@ -1932,7 +1932,193 @@
|
|
|
1932
1932
|
"tasks",
|
|
1933
1933
|
"today.js"
|
|
1934
1934
|
]
|
|
1935
|
+
},
|
|
1936
|
+
"vuln:detail": {
|
|
1937
|
+
"aliases": [],
|
|
1938
|
+
"args": {
|
|
1939
|
+
"cveId": {
|
|
1940
|
+
"description": "CVE identifier (e.g. CVE-2021-44228)",
|
|
1941
|
+
"name": "cveId",
|
|
1942
|
+
"required": true
|
|
1943
|
+
}
|
|
1944
|
+
},
|
|
1945
|
+
"description": "View full details for a specific CVE",
|
|
1946
|
+
"examples": [
|
|
1947
|
+
"<%= config.bin %> vuln detail CVE-2021-44228",
|
|
1948
|
+
"<%= config.bin %> vuln detail CVE-2021-44228 --open",
|
|
1949
|
+
"<%= config.bin %> vuln detail CVE-2021-44228 --json"
|
|
1950
|
+
],
|
|
1951
|
+
"flags": {
|
|
1952
|
+
"json": {
|
|
1953
|
+
"description": "Format output as json.",
|
|
1954
|
+
"helpGroup": "GLOBAL",
|
|
1955
|
+
"name": "json",
|
|
1956
|
+
"allowNo": false,
|
|
1957
|
+
"type": "boolean"
|
|
1958
|
+
},
|
|
1959
|
+
"open": {
|
|
1960
|
+
"char": "o",
|
|
1961
|
+
"description": "Open the first reference URL in the default browser",
|
|
1962
|
+
"name": "open",
|
|
1963
|
+
"allowNo": false,
|
|
1964
|
+
"type": "boolean"
|
|
1965
|
+
}
|
|
1966
|
+
},
|
|
1967
|
+
"hasDynamicHelp": false,
|
|
1968
|
+
"hiddenAliases": [],
|
|
1969
|
+
"id": "vuln:detail",
|
|
1970
|
+
"pluginAlias": "devvami",
|
|
1971
|
+
"pluginName": "devvami",
|
|
1972
|
+
"pluginType": "core",
|
|
1973
|
+
"strict": true,
|
|
1974
|
+
"enableJsonFlag": true,
|
|
1975
|
+
"isESM": true,
|
|
1976
|
+
"relativePath": [
|
|
1977
|
+
"src",
|
|
1978
|
+
"commands",
|
|
1979
|
+
"vuln",
|
|
1980
|
+
"detail.js"
|
|
1981
|
+
]
|
|
1982
|
+
},
|
|
1983
|
+
"vuln:scan": {
|
|
1984
|
+
"aliases": [],
|
|
1985
|
+
"args": {},
|
|
1986
|
+
"description": "Scan the current directory for known vulnerabilities in dependencies",
|
|
1987
|
+
"examples": [
|
|
1988
|
+
"<%= config.bin %> vuln scan",
|
|
1989
|
+
"<%= config.bin %> vuln scan --severity high",
|
|
1990
|
+
"<%= config.bin %> vuln scan --no-fail",
|
|
1991
|
+
"<%= config.bin %> vuln scan --report vuln-report.md",
|
|
1992
|
+
"<%= config.bin %> vuln scan --json"
|
|
1993
|
+
],
|
|
1994
|
+
"flags": {
|
|
1995
|
+
"json": {
|
|
1996
|
+
"description": "Format output as json.",
|
|
1997
|
+
"helpGroup": "GLOBAL",
|
|
1998
|
+
"name": "json",
|
|
1999
|
+
"allowNo": false,
|
|
2000
|
+
"type": "boolean"
|
|
2001
|
+
},
|
|
2002
|
+
"severity": {
|
|
2003
|
+
"char": "s",
|
|
2004
|
+
"description": "Minimum severity filter",
|
|
2005
|
+
"name": "severity",
|
|
2006
|
+
"hasDynamicHelp": false,
|
|
2007
|
+
"multiple": false,
|
|
2008
|
+
"options": [
|
|
2009
|
+
"low",
|
|
2010
|
+
"medium",
|
|
2011
|
+
"high",
|
|
2012
|
+
"critical"
|
|
2013
|
+
],
|
|
2014
|
+
"type": "option"
|
|
2015
|
+
},
|
|
2016
|
+
"no-fail": {
|
|
2017
|
+
"description": "Exit with code 0 even when vulnerabilities are found",
|
|
2018
|
+
"name": "no-fail",
|
|
2019
|
+
"allowNo": false,
|
|
2020
|
+
"type": "boolean"
|
|
2021
|
+
},
|
|
2022
|
+
"report": {
|
|
2023
|
+
"char": "r",
|
|
2024
|
+
"description": "Export vulnerability report to file path (Markdown format)",
|
|
2025
|
+
"name": "report",
|
|
2026
|
+
"hasDynamicHelp": false,
|
|
2027
|
+
"multiple": false,
|
|
2028
|
+
"type": "option"
|
|
2029
|
+
}
|
|
2030
|
+
},
|
|
2031
|
+
"hasDynamicHelp": false,
|
|
2032
|
+
"hiddenAliases": [],
|
|
2033
|
+
"id": "vuln:scan",
|
|
2034
|
+
"pluginAlias": "devvami",
|
|
2035
|
+
"pluginName": "devvami",
|
|
2036
|
+
"pluginType": "core",
|
|
2037
|
+
"strict": true,
|
|
2038
|
+
"enableJsonFlag": true,
|
|
2039
|
+
"isESM": true,
|
|
2040
|
+
"relativePath": [
|
|
2041
|
+
"src",
|
|
2042
|
+
"commands",
|
|
2043
|
+
"vuln",
|
|
2044
|
+
"scan.js"
|
|
2045
|
+
]
|
|
2046
|
+
},
|
|
2047
|
+
"vuln:search": {
|
|
2048
|
+
"aliases": [],
|
|
2049
|
+
"args": {
|
|
2050
|
+
"keyword": {
|
|
2051
|
+
"description": "Product, library, or keyword to search for (optional — omit to see all recent CVEs)",
|
|
2052
|
+
"name": "keyword",
|
|
2053
|
+
"required": false
|
|
2054
|
+
}
|
|
2055
|
+
},
|
|
2056
|
+
"description": "Search for recent CVEs by keyword (omit keyword to see all recent CVEs)",
|
|
2057
|
+
"examples": [
|
|
2058
|
+
"<%= config.bin %> vuln search openssl",
|
|
2059
|
+
"<%= config.bin %> vuln search openssl --days 30",
|
|
2060
|
+
"<%= config.bin %> vuln search log4j --severity critical",
|
|
2061
|
+
"<%= config.bin %> vuln search nginx --limit 10 --json",
|
|
2062
|
+
"<%= config.bin %> vuln search",
|
|
2063
|
+
"<%= config.bin %> vuln search --days 7 --severity high"
|
|
2064
|
+
],
|
|
2065
|
+
"flags": {
|
|
2066
|
+
"json": {
|
|
2067
|
+
"description": "Format output as json.",
|
|
2068
|
+
"helpGroup": "GLOBAL",
|
|
2069
|
+
"name": "json",
|
|
2070
|
+
"allowNo": false,
|
|
2071
|
+
"type": "boolean"
|
|
2072
|
+
},
|
|
2073
|
+
"days": {
|
|
2074
|
+
"char": "d",
|
|
2075
|
+
"description": "Time window in days (search CVEs published within last N days)",
|
|
2076
|
+
"name": "days",
|
|
2077
|
+
"default": 14,
|
|
2078
|
+
"hasDynamicHelp": false,
|
|
2079
|
+
"multiple": false,
|
|
2080
|
+
"type": "option"
|
|
2081
|
+
},
|
|
2082
|
+
"severity": {
|
|
2083
|
+
"char": "s",
|
|
2084
|
+
"description": "Minimum severity filter",
|
|
2085
|
+
"name": "severity",
|
|
2086
|
+
"hasDynamicHelp": false,
|
|
2087
|
+
"multiple": false,
|
|
2088
|
+
"options": [
|
|
2089
|
+
"low",
|
|
2090
|
+
"medium",
|
|
2091
|
+
"high",
|
|
2092
|
+
"critical"
|
|
2093
|
+
],
|
|
2094
|
+
"type": "option"
|
|
2095
|
+
},
|
|
2096
|
+
"limit": {
|
|
2097
|
+
"char": "l",
|
|
2098
|
+
"description": "Maximum number of results to display",
|
|
2099
|
+
"name": "limit",
|
|
2100
|
+
"default": 20,
|
|
2101
|
+
"hasDynamicHelp": false,
|
|
2102
|
+
"multiple": false,
|
|
2103
|
+
"type": "option"
|
|
2104
|
+
}
|
|
2105
|
+
},
|
|
2106
|
+
"hasDynamicHelp": false,
|
|
2107
|
+
"hiddenAliases": [],
|
|
2108
|
+
"id": "vuln:search",
|
|
2109
|
+
"pluginAlias": "devvami",
|
|
2110
|
+
"pluginName": "devvami",
|
|
2111
|
+
"pluginType": "core",
|
|
2112
|
+
"strict": true,
|
|
2113
|
+
"enableJsonFlag": true,
|
|
2114
|
+
"isESM": true,
|
|
2115
|
+
"relativePath": [
|
|
2116
|
+
"src",
|
|
2117
|
+
"commands",
|
|
2118
|
+
"vuln",
|
|
2119
|
+
"search.js"
|
|
2120
|
+
]
|
|
1935
2121
|
}
|
|
1936
2122
|
},
|
|
1937
|
-
"version": "1.4.
|
|
2123
|
+
"version": "1.4.2"
|
|
1938
2124
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Command, Args, Flags } from '@oclif/core'
|
|
2
|
+
import ora from 'ora'
|
|
3
|
+
import { getCveDetail } from '../../services/nvd.js'
|
|
4
|
+
import { formatCveDetail } from '../../formatters/vuln.js'
|
|
5
|
+
import { openBrowser } from '../../utils/open-browser.js'
|
|
6
|
+
import { ValidationError } from '../../utils/errors.js'
|
|
7
|
+
|
|
8
|
+
export default class VulnDetail extends Command {
|
|
9
|
+
static description = 'View full details for a specific CVE'
|
|
10
|
+
|
|
11
|
+
static examples = [
|
|
12
|
+
'<%= config.bin %> vuln detail CVE-2021-44228',
|
|
13
|
+
'<%= config.bin %> vuln detail CVE-2021-44228 --open',
|
|
14
|
+
'<%= config.bin %> vuln detail CVE-2021-44228 --json',
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
static enableJsonFlag = true
|
|
18
|
+
|
|
19
|
+
static args = {
|
|
20
|
+
cveId: Args.string({ description: 'CVE identifier (e.g. CVE-2021-44228)', required: true }),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static flags = {
|
|
24
|
+
open: Flags.boolean({
|
|
25
|
+
char: 'o',
|
|
26
|
+
description: 'Open the first reference URL in the default browser',
|
|
27
|
+
default: false,
|
|
28
|
+
}),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async run() {
|
|
32
|
+
const { args, flags } = await this.parse(VulnDetail)
|
|
33
|
+
const isJson = flags.json
|
|
34
|
+
const { cveId } = args
|
|
35
|
+
|
|
36
|
+
if (!cveId || !/^CVE-\d{4}-\d{4,}$/i.test(cveId)) {
|
|
37
|
+
throw new ValidationError(
|
|
38
|
+
`Invalid CVE ID: ${cveId}`,
|
|
39
|
+
'CVE IDs must match the format CVE-YYYY-NNNNN (e.g. CVE-2021-44228)',
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const spinner = isJson ? null : ora(`Fetching ${cveId.toUpperCase()}...`).start()
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const detail = await getCveDetail(cveId)
|
|
47
|
+
spinner?.stop()
|
|
48
|
+
|
|
49
|
+
if (isJson) return detail
|
|
50
|
+
|
|
51
|
+
this.log(formatCveDetail(detail))
|
|
52
|
+
|
|
53
|
+
if (flags.open && detail.references.length > 0) {
|
|
54
|
+
const firstUrl = detail.references[0].url
|
|
55
|
+
this.log(`\nOpening ${firstUrl} ...`)
|
|
56
|
+
await openBrowser(firstUrl)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return detail
|
|
60
|
+
} catch (err) {
|
|
61
|
+
spinner?.stop()
|
|
62
|
+
throw err
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core'
|
|
2
|
+
import { writeFile } from 'node:fs/promises'
|
|
3
|
+
import ora from 'ora'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
import { detectEcosystems, supportedEcosystemsMessage } from '../../services/audit-detector.js'
|
|
6
|
+
import { runAudit, summarizeFindings, filterBySeverity } from '../../services/audit-runner.js'
|
|
7
|
+
import { formatFindingsTable, formatScanSummary, formatMarkdownReport, truncate, colorSeverity } from '../../formatters/vuln.js'
|
|
8
|
+
import { getCveDetail } from '../../services/nvd.js'
|
|
9
|
+
import { startInteractiveTable } from '../../utils/tui/navigable-table.js'
|
|
10
|
+
|
|
11
|
+
// Minimum terminal rows required to show the interactive TUI (same threshold as vuln search)
|
|
12
|
+
const MIN_TTY_ROWS = 6
|
|
13
|
+
|
|
14
|
+
// Column widths for the navigable table (match the static findings table)
|
|
15
|
+
const COL_WIDTHS = {
|
|
16
|
+
pkg: 20,
|
|
17
|
+
version: 12,
|
|
18
|
+
severity: 10,
|
|
19
|
+
cve: 20,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default class VulnScan extends Command {
|
|
23
|
+
static description = 'Scan the current directory for known vulnerabilities in dependencies'
|
|
24
|
+
|
|
25
|
+
static examples = [
|
|
26
|
+
'<%= config.bin %> vuln scan',
|
|
27
|
+
'<%= config.bin %> vuln scan --severity high',
|
|
28
|
+
'<%= config.bin %> vuln scan --no-fail',
|
|
29
|
+
'<%= config.bin %> vuln scan --report vuln-report.md',
|
|
30
|
+
'<%= config.bin %> vuln scan --json',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
static enableJsonFlag = true
|
|
34
|
+
|
|
35
|
+
static flags = {
|
|
36
|
+
severity: Flags.string({
|
|
37
|
+
char: 's',
|
|
38
|
+
description: 'Minimum severity filter',
|
|
39
|
+
options: ['low', 'medium', 'high', 'critical'],
|
|
40
|
+
}),
|
|
41
|
+
'no-fail': Flags.boolean({
|
|
42
|
+
description: 'Exit with code 0 even when vulnerabilities are found',
|
|
43
|
+
default: false,
|
|
44
|
+
}),
|
|
45
|
+
report: Flags.string({
|
|
46
|
+
char: 'r',
|
|
47
|
+
description: 'Export vulnerability report to file path (Markdown format)',
|
|
48
|
+
}),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async run() {
|
|
52
|
+
const { flags } = await this.parse(VulnScan)
|
|
53
|
+
const isJson = flags.json
|
|
54
|
+
const { severity, 'no-fail': noFail, report } = flags
|
|
55
|
+
|
|
56
|
+
const projectPath = process.env.DVMI_SCAN_DIR ?? process.cwd()
|
|
57
|
+
const scanDate = new Date().toISOString()
|
|
58
|
+
|
|
59
|
+
// Detect ecosystems
|
|
60
|
+
const ecosystems = detectEcosystems(projectPath)
|
|
61
|
+
|
|
62
|
+
if (ecosystems.length === 0) {
|
|
63
|
+
if (isJson) {
|
|
64
|
+
return {
|
|
65
|
+
projectPath,
|
|
66
|
+
scanDate,
|
|
67
|
+
ecosystems: [],
|
|
68
|
+
findings: [],
|
|
69
|
+
summary: { critical: 0, high: 0, medium: 0, low: 0, unknown: 0, total: 0 },
|
|
70
|
+
errors: [{ ecosystem: 'none', message: 'No supported package manager detected.' }],
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.log(chalk.red(' ✘ No supported package manager detected.'))
|
|
75
|
+
this.log('')
|
|
76
|
+
this.log(' Supported ecosystems:')
|
|
77
|
+
this.log(supportedEcosystemsMessage())
|
|
78
|
+
this.log('')
|
|
79
|
+
this.log(' Tip: Make sure you have a lock file in the current directory.')
|
|
80
|
+
this.exit(2)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Display detected ecosystems
|
|
85
|
+
if (!isJson) {
|
|
86
|
+
this.log(chalk.bold('Vulnerability Scan'))
|
|
87
|
+
this.log('')
|
|
88
|
+
this.log(' Detected ecosystems:')
|
|
89
|
+
for (const eco of ecosystems) {
|
|
90
|
+
this.log(` ${chalk.green('●')} ${eco.name} (${eco.lockFile})`)
|
|
91
|
+
}
|
|
92
|
+
this.log('')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Run audits
|
|
96
|
+
const allFindings = []
|
|
97
|
+
const errors = []
|
|
98
|
+
|
|
99
|
+
for (const eco of ecosystems) {
|
|
100
|
+
const spinner = isJson ? null : ora(` Scanning ${eco.name} dependencies...`).start()
|
|
101
|
+
|
|
102
|
+
const { findings, error } = await runAudit(eco)
|
|
103
|
+
|
|
104
|
+
if (error) {
|
|
105
|
+
spinner?.fail(` Scanning ${eco.name} dependencies... failed`)
|
|
106
|
+
errors.push({ ecosystem: eco.name, message: error })
|
|
107
|
+
} else {
|
|
108
|
+
spinner?.succeed(` Scanning ${eco.name} dependencies... done`)
|
|
109
|
+
allFindings.push(...findings)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Apply severity filter
|
|
114
|
+
const filteredFindings = filterBySeverity(allFindings, severity)
|
|
115
|
+
|
|
116
|
+
// Build summary
|
|
117
|
+
const summary = summarizeFindings(filteredFindings)
|
|
118
|
+
|
|
119
|
+
const result = {
|
|
120
|
+
projectPath,
|
|
121
|
+
scanDate,
|
|
122
|
+
ecosystems,
|
|
123
|
+
findings: filteredFindings,
|
|
124
|
+
summary,
|
|
125
|
+
errors,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Write report if requested (always, regardless of TTY mode)
|
|
129
|
+
if (report) {
|
|
130
|
+
const markdown = formatMarkdownReport(result)
|
|
131
|
+
await writeFile(report, markdown, 'utf8')
|
|
132
|
+
if (!isJson) this.log(`\n Report saved to: ${report}`)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (isJson) return result
|
|
136
|
+
|
|
137
|
+
this.log('')
|
|
138
|
+
|
|
139
|
+
if (filteredFindings.length === 0 && errors.length === 0) {
|
|
140
|
+
this.log(chalk.green(' ✔ No known vulnerabilities found.'))
|
|
141
|
+
return result
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── TTY interactive table ──────────────────────────────────────────────────
|
|
145
|
+
// In a real TTY with enough rows and at least one finding, replace the static
|
|
146
|
+
// table with the navigable TUI (same experience as `dvmi vuln search`).
|
|
147
|
+
const ttyRows = process.stdout.rows ?? 0
|
|
148
|
+
const useTUI = process.stdout.isTTY && filteredFindings.length > 0 && ttyRows >= MIN_TTY_ROWS
|
|
149
|
+
|
|
150
|
+
if (useTUI) {
|
|
151
|
+
const count = filteredFindings.length
|
|
152
|
+
const label = count === 1 ? 'finding' : 'findings'
|
|
153
|
+
const heading = `Vulnerability Scan: ${count} ${label}`
|
|
154
|
+
|
|
155
|
+
const termCols = process.stdout.columns || 80
|
|
156
|
+
// Title width: whatever is left after Package + Version + Severity + CVE + separators
|
|
157
|
+
const fixedCols = COL_WIDTHS.pkg + COL_WIDTHS.version + COL_WIDTHS.severity + COL_WIDTHS.cve
|
|
158
|
+
const separators = 5 * 2 // 5 gaps between 5 columns
|
|
159
|
+
const titleWidth = Math.max(15, Math.min(50, termCols - fixedCols - separators))
|
|
160
|
+
|
|
161
|
+
const rows = filteredFindings.map((f) => ({
|
|
162
|
+
id: f.cveId ?? null,
|
|
163
|
+
pkg: f.package,
|
|
164
|
+
version: f.installedVersion ?? '—',
|
|
165
|
+
severity: f.severity,
|
|
166
|
+
cve: f.cveId ?? '—',
|
|
167
|
+
title: truncate(f.title ?? '—', titleWidth),
|
|
168
|
+
advisoryUrl: f.advisoryUrl ?? null,
|
|
169
|
+
}))
|
|
170
|
+
|
|
171
|
+
/** @type {import('../../utils/tui/navigable-table.js').TableColumnDef[]} */
|
|
172
|
+
const columns = [
|
|
173
|
+
{ header: 'Package', key: 'pkg', width: COL_WIDTHS.pkg },
|
|
174
|
+
{ header: 'Version', key: 'version', width: COL_WIDTHS.version },
|
|
175
|
+
{ header: 'Severity', key: 'severity', width: COL_WIDTHS.severity, colorize: (v) => colorSeverity(v) },
|
|
176
|
+
{ header: 'CVE', key: 'cve', width: COL_WIDTHS.cve, colorize: (v) => (v !== '—' ? chalk.cyan(v) : chalk.gray(v)) },
|
|
177
|
+
{ header: 'Title', key: 'title', width: titleWidth },
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
await startInteractiveTable(rows, columns, heading, filteredFindings.length, getCveDetail)
|
|
181
|
+
} else {
|
|
182
|
+
// Non-TTY fallback: static table + summary (unchanged from pre-TUI behaviour)
|
|
183
|
+
if (filteredFindings.length > 0) {
|
|
184
|
+
this.log(chalk.bold(` Findings (${filteredFindings.length} ${filteredFindings.length === 1 ? 'vulnerability' : 'vulnerabilities'})`))
|
|
185
|
+
this.log('')
|
|
186
|
+
this.log(formatFindingsTable(filteredFindings))
|
|
187
|
+
this.log('')
|
|
188
|
+
this.log(chalk.bold(' Summary'))
|
|
189
|
+
this.log(formatScanSummary(summary))
|
|
190
|
+
this.log('')
|
|
191
|
+
this.log(chalk.yellow(` ⚠ ${filteredFindings.length} ${filteredFindings.length === 1 ? 'vulnerability' : 'vulnerabilities'} found. Run \`dvmi vuln detail <CVE-ID>\` for details.`))
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Always print audit errors (e.g. tool not installed) after findings/TUI
|
|
196
|
+
if (errors.length > 0) {
|
|
197
|
+
this.log('')
|
|
198
|
+
for (const err of errors) {
|
|
199
|
+
this.log(chalk.red(` ✘ ${err.ecosystem}: ${err.message}`))
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Preserve exit code semantics: exit 1 when vulns found (unless --no-fail)
|
|
204
|
+
if (filteredFindings.length > 0 && !noFail) {
|
|
205
|
+
this.exit(1)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return result
|
|
209
|
+
}
|
|
210
|
+
}
|