vtk 1.1.0 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c41b577facc94e32eb15aeea7f5817a49f54c31a568654c5dfe5da082d0775f9
4
- data.tar.gz: 87820698519528ff97594bf9e148a0a20ca700026079a0fc2b65ede3bee84b19
3
+ metadata.gz: '0585f00ec9fe122942c5a249fd2733e47baa461960cc9d106e2dfb9ac13dc3aa'
4
+ data.tar.gz: c23b728a27d519e15812f8b95ffcf7e0aac20981e38cab59dca62c4a62582f21
5
5
  SHA512:
6
- metadata.gz: 729f7b3a93dd61661ad557d471796a059ada830970afe16bbeef7bdf84e0e92887a321249c5ffc025db051fe71dfffb00fdec836382e3041392ae3ddcc9e5b59
7
- data.tar.gz: eb5411ac4f245210470d310767fabd2e877759a5dcb023b00ae4925e8dcb4dec16f7637441433ca978d809d807734b18e4e51d901f69107e8291d186309d2ba3
6
+ metadata.gz: 0d10b96103db87af8754a1a1a3cb0fc6a268255f9c596baccf730f73c8a8ba03598a84cdcc8de35a53114710fe546e572a8f7ca4706efc8c45e7de3ce28aa115
7
+ data.tar.gz: 9e78a14e489d84f99a81725f7f93c03c3356d7165d7d79c871ade2cb61f2d462f8dae315f8b1f570ae7db4e84f0e9f0b4d7706f4d06dcc2c4622272025edf18d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [v1.2.0](https://github.com/department-of-veterans-affairs/vtk/tree/v1.2.0) (2026-01-09)
4
+
5
+ [Full Changelog](https://github.com/department-of-veterans-affairs/vtk/compare/v1.1.0...v1.2.0)
6
+
7
+ **Merged pull requests:**
8
+
9
+ - feat(scan): add PowerShell scripts for Windows users [\#69](https://github.com/department-of-veterans-affairs/vtk/pull/69) ([ericboehs](https://github.com/ericboehs))
10
+ - feat(scan): add vtk scan credentials for security incident response [\#68](https://github.com/department-of-veterans-affairs/vtk/pull/68) ([ericboehs](https://github.com/ericboehs))
11
+ - feat(scan): add vtk scan repo for compromised package detection [\#65](https://github.com/department-of-veterans-affairs/vtk/pull/65) ([ericboehs](https://github.com/ericboehs))
12
+
3
13
  ## [v1.1.0](https://github.com/department-of-veterans-affairs/vtk/tree/v1.1.0) (2025-12-15)
4
14
 
5
15
  [Full Changelog](https://github.com/department-of-veterans-affairs/vtk/compare/v1.0.0...v1.1.0)
data/README.md CHANGED
@@ -98,6 +98,81 @@ Example:
98
98
  $ vtk scan machine --quiet && echo "Clean" || echo "Check machine!"
99
99
  ```
100
100
 
101
+ #### Windows (PowerShell)
102
+
103
+ For Windows users, standalone PowerShell scripts are available that don't require Ruby or the vtk gem.
104
+
105
+ **Requirements:** PowerShell 5.1+ (ships with Windows 10/11). PowerShell 7+ recommended for best experience.
106
+
107
+ **Download and run:**
108
+
109
+ ```powershell
110
+ # Machine scanner - checks for active infection
111
+ Invoke-WebRequest -Uri "https://raw.githubusercontent.com/department-of-veterans-affairs/vtk/master/scripts/shai-hulud-machine-check.ps1" -OutFile "shai-hulud-machine-check.ps1"
112
+
113
+ # Allow script execution (if needed)
114
+ Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
115
+
116
+ # Run the scanner
117
+ .\shai-hulud-machine-check.ps1
118
+ .\shai-hulud-machine-check.ps1 -Verbose # Detailed output
119
+ .\shai-hulud-machine-check.ps1 -Json # JSON output
120
+ ```
121
+
122
+ **Repository scanner** - checks lockfiles for compromised packages:
123
+
124
+ ```powershell
125
+ Invoke-WebRequest -Uri "https://raw.githubusercontent.com/department-of-veterans-affairs/vtk/master/scripts/shai-hulud-repo-check.ps1" -OutFile "shai-hulud-repo-check.ps1"
126
+
127
+ .\shai-hulud-repo-check.ps1 # Scan current directory
128
+ .\shai-hulud-repo-check.ps1 -Path C:\Code\app # Scan specific directory
129
+ .\shai-hulud-repo-check.ps1 -Recursive # Recursive scan
130
+ ```
131
+
132
+ **Credential audit** - lists credentials that may need rotation:
133
+
134
+ ```powershell
135
+ Invoke-WebRequest -Uri "https://raw.githubusercontent.com/department-of-veterans-affairs/vtk/master/scripts/credential-audit.ps1" -OutFile "credential-audit.ps1"
136
+
137
+ .\credential-audit.ps1
138
+ .\credential-audit.ps1 -Verbose # Show all checks
139
+ ```
140
+
141
+ ---
142
+
143
+ ```
144
+ $ vtk scan credentials
145
+ ```
146
+
147
+ The **credentials subcommand** audits which credentials are present on your machine and provides rotation instructions for each. Run this after a suspected or confirmed security incident.
148
+
149
+ **What it checks:**
150
+ - **NPM**: `~/.npmrc`, `$NPM_TOKEN`, `$NPM_CONFIG_TOKEN`
151
+ - **AWS**: `~/.aws/credentials`, `~/.aws/config`, `$AWS_ACCESS_KEY_ID`, `$AWS_SECRET_ACCESS_KEY`
152
+ - **GCP**: `~/.config/gcloud/application_default_credentials.json`, `$GOOGLE_APPLICATION_CREDENTIALS`
153
+ - **Azure**: `~/.azure/` directory, `$AZURE_CLIENT_SECRET`
154
+ - **GitHub**: `~/.config/gh/hosts.yml`, `$GITHUB_TOKEN`, `$GH_TOKEN`, `~/.git-credentials`
155
+ - **SSH**: Private keys in `~/.ssh/`
156
+ - **Docker**: `~/.docker/config.json`
157
+ - **Kubernetes**: `~/.kube/config`
158
+ - **Environment**: Sensitive env vars (token, secret, password, etc.)
159
+
160
+ **Exit codes:**
161
+ - `0` - No credentials found
162
+ - `1` - Credentials found (rotation recommended)
163
+
164
+ **Options:**
165
+ - `--verbose` / `-v` - Show all checks including clean ones
166
+ - `--json` / `-j` - JSON output format
167
+
168
+ Example:
169
+ ```
170
+ $ vtk scan credentials --json | jq -r '.credentials[].service' | sort -u
171
+ AWS
172
+ GitHub
173
+ SSH
174
+ ```
175
+
101
176
  ---
102
177
 
103
178
  ### Help
@@ -0,0 +1,102 @@
1
+ # VTK Scan Commands
2
+
3
+ Security scanning commands for detecting Shai-Hulud malware and supply chain compromises.
4
+
5
+ ## Commands
6
+
7
+ ### `vtk scan machine`
8
+
9
+ Quick check for active malware infection on your developer machine.
10
+
11
+ ```bash
12
+ vtk scan machine # Compact output
13
+ vtk scan machine --verbose # Detailed checks
14
+ vtk scan machine --json # JSON output
15
+ vtk scan machine --quiet # Exit code only
16
+ ```
17
+
18
+ **What it checks:**
19
+ - `~/.dev-env/` persistence folder (critical indicator)
20
+ - Malicious processes (Runner.Listener, SHA1HULUD)
21
+ - Malware files (setup_bun.js, bun_environment.js)
22
+ - Exfiltration artifacts
23
+ - Unexpected Bun/Trufflehog installations
24
+
25
+ **Exit codes:**
26
+ - `0` - Clean
27
+ - `1` - Infected (critical indicators found)
28
+ - `2` - Warning (needs investigation)
29
+
30
+ ### `vtk scan repo [PATH]`
31
+
32
+ Scan a repository for compromised npm packages and backdoor workflows.
33
+
34
+ ```bash
35
+ vtk scan repo # Scan current directory
36
+ vtk scan repo /path/to/repo # Scan specific path
37
+ vtk scan repo -r # Recursive scan (default depth: 5)
38
+ vtk scan repo -r --depth=10 # Recursive with custom depth
39
+ vtk scan repo -r --depth=0 # Recursive with unlimited depth
40
+ vtk scan repo -v # Verbose output (show each lockfile)
41
+ vtk scan repo --refresh # Force refresh package list
42
+ vtk scan repo --json # JSON output
43
+ vtk scan repo -r --json # JSONL output (streaming)
44
+ vtk scan repo --quiet # Exit code only
45
+ ```
46
+
47
+ **What it checks:**
48
+ - Lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml) against 1,600+ known compromised packages
49
+ - Backdoor workflows (.github/workflows/discussion.yaml)
50
+ - Secrets extraction workflows (.github/workflows/formatter_*.yml)
51
+
52
+ **Exit codes:**
53
+ - `0` - Clean
54
+ - `1` - Compromised packages found
55
+ - `2` - Backdoor workflow found
56
+
57
+ **Output formats:**
58
+
59
+ JSON (`--json`) - Single JSON object for non-recursive scans:
60
+ ```json
61
+ {"path":"/repo","status":"CLEAN","packages_scanned":2595,"compromised_packages":[],...}
62
+ ```
63
+
64
+ JSONL (`-r --json`) - One JSON line per lockfile as scanned (streaming):
65
+ ```
66
+ {"lockfile":"/repo/yarn.lock","packages_scanned":2595,"status":"CLEAN","compromised_packages":[]}
67
+ {"lockfile":"/repo/app/package-lock.json","packages_scanned":100,"status":"CLEAN","compromised_packages":[]}
68
+ {"type":"summary","status":"CLEAN","total_packages_scanned":2695,...}
69
+ ```
70
+
71
+ ## Data Sources
72
+
73
+ The compromised packages list is fetched from [Cobenian/shai-hulud-detect](https://github.com/Cobenian/shai-hulud-detect) and cached locally for 24 hours.
74
+
75
+ **Cache location:** `$XDG_CACHE_HOME/vtk/compromised-packages.txt` (defaults to `~/.cache/vtk/`)
76
+
77
+ ## Standalone Scripts
78
+
79
+ Both scan commands are available as standalone shell scripts that work without vtk installed. These are useful for CI/CD pipelines or machines without Ruby.
80
+
81
+ ```bash
82
+ # Machine scan
83
+ ./scripts/shai-hulud-machine-check.sh
84
+ ./scripts/shai-hulud-machine-check.sh --json
85
+
86
+ # Repo scan
87
+ ./scripts/shai-hulud-repo-check.sh /path/to/repo
88
+ ./scripts/shai-hulud-repo-check.sh -r ~/Code # Scan all projects
89
+ ./scripts/shai-hulud-repo-check.sh -r --json # JSONL output
90
+ ```
91
+
92
+ ## Known Limitations
93
+
94
+ - **pnpm-lock.yaml** - Parsing tested with pnpm v6/v7/v9; other versions may vary
95
+ - **node_modules** - Not scanned directly; lockfile is the source of truth (by design)
96
+ - **Short flags** - Cannot be combined in shell script (use `-r -j` not `-rj`)
97
+
98
+ ## References
99
+
100
+ - [Datadog: Shai-Hulud 2.0 npm Worm](https://securitylabs.datadoghq.com/articles/shai-hulud-2.0-npm-worm/)
101
+ - [Cobenian/shai-hulud-detect](https://github.com/Cobenian/shai-hulud-detect)
102
+ - [VA Cleanup Playbook](https://department-of-veterans-affairs.github.io/eert/shai-hulud-dev-machine-cleanup-playbook)
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ require_relative '../../command'
5
+
6
+ module Vtk
7
+ module Commands
8
+ class Scan
9
+ # Audit credentials that may have been accessed by Shai-Hulud malware
10
+ class Credentials < Vtk::Command
11
+ attr_reader :options
12
+
13
+ def initialize(options)
14
+ @options = options
15
+ super()
16
+ end
17
+
18
+ OPTION_FLAGS = {
19
+ verbose: '--verbose',
20
+ json: '--json'
21
+ }.freeze
22
+
23
+ def execute(output: $stdout)
24
+ @output = output
25
+
26
+ script_path, gem_root = find_script
27
+ return script_not_found(output, gem_root) unless script_path
28
+
29
+ run_script(script_path)
30
+ end
31
+
32
+ private
33
+
34
+ def script_not_found(output, gem_root)
35
+ output.puts 'ERROR: Could not find credential-audit.sh script'
36
+ output.puts "Expected at: #{gem_root}/scripts/credential-audit.sh"
37
+ 1
38
+ end
39
+
40
+ def run_script(script_path)
41
+ cmd = ['bash', script_path]
42
+ cmd += OPTION_FLAGS.filter_map { |key, flag| flag if options[key] }
43
+
44
+ system(*cmd)
45
+ $CHILD_STATUS.exitstatus
46
+ end
47
+
48
+ def find_script
49
+ gem_root = File.expand_path('../../../..', __dir__)
50
+ script_path = File.join(gem_root, 'scripts', 'credential-audit.sh')
51
+
52
+ return [script_path, gem_root] if File.exist?(script_path)
53
+
54
+ [nil, gem_root]
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ require_relative '../../command'
5
+
6
+ module Vtk
7
+ module Commands
8
+ class Scan
9
+ # Scan a repository for compromised packages and backdoor workflows
10
+ # Shells out to shai-hulud-repo-check.sh for the actual scanning
11
+ class Repo < Vtk::Command
12
+ OPTION_FLAGS = {
13
+ refresh: '--refresh',
14
+ json: '--json',
15
+ quiet: '--quiet',
16
+ verbose: '--verbose',
17
+ recursive: '--recursive'
18
+ }.freeze
19
+
20
+ attr_reader :options, :path
21
+
22
+ def initialize(path, options)
23
+ @path = path || Dir.pwd
24
+ @options = options
25
+ super()
26
+ end
27
+
28
+ def execute(output: $stdout)
29
+ @output = output
30
+
31
+ unless File.directory?(@path)
32
+ output.puts "ERROR: Directory not found: #{@path}"
33
+ return 1
34
+ end
35
+
36
+ script_path, gem_root = find_script
37
+ return script_not_found(output, gem_root) unless script_path
38
+
39
+ run_script(script_path)
40
+ end
41
+
42
+ private
43
+
44
+ def script_not_found(output, gem_root)
45
+ output.puts 'ERROR: Could not find shai-hulud-repo-check.sh script'
46
+ output.puts "Expected at: #{gem_root}/scripts/shai-hulud-repo-check.sh"
47
+ 1
48
+ end
49
+
50
+ def run_script(script_path)
51
+ cmd = ['bash', script_path] + script_options + [@path]
52
+ system(*cmd)
53
+ $CHILD_STATUS.exitstatus
54
+ end
55
+
56
+ def script_options
57
+ # Use select + map instead of filter_map for Ruby 2.6 compatibility
58
+ flags = OPTION_FLAGS.select { |key, _flag| options[key] }.values
59
+ flags << "--depth=#{options[:depth]}" if options[:depth]
60
+ flags
61
+ end
62
+
63
+ def find_script
64
+ # Look for script relative to this gem's location
65
+ # __dir__ = lib/vtk/commands/scan, so go up 4 levels to get to vtk root
66
+ gem_root = File.expand_path('../../../..', __dir__)
67
+ script_path = File.join(gem_root, 'scripts', 'shai-hulud-repo-check.sh')
68
+
69
+ # Use explicit bash interpreter, so executable bit not required
70
+ return [script_path, gem_root] if File.exist?(script_path)
71
+
72
+ [nil, gem_root]
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -29,9 +29,47 @@ module Vtk
29
29
  end
30
30
  end
31
31
 
32
- # Future subcommands:
33
- # desc 'repos', 'Scan lockfiles for compromised packages'
34
- # desc 'credentials', 'Inventory credentials that may need rotation'
32
+ desc 'repo [PATH]', 'Scan a repository for compromised packages'
33
+ method_option :help, aliases: '-h', type: :boolean,
34
+ desc: 'Display usage information'
35
+ method_option :refresh, type: :boolean,
36
+ desc: 'Force refresh of compromised packages list'
37
+ method_option :json, aliases: '-j', type: :boolean,
38
+ desc: 'Output results as JSON'
39
+ method_option :quiet, aliases: '-q', type: :boolean,
40
+ desc: 'Exit code only, no output'
41
+ method_option :verbose, aliases: '-v', type: :boolean,
42
+ desc: 'Show each lockfile as it is scanned'
43
+ method_option :recursive, aliases: '-r', type: :boolean,
44
+ desc: 'Recursively scan subdirectories (default depth: 5)'
45
+ method_option :depth, type: :numeric, default: 5,
46
+ desc: 'Max directory depth for recursive scan (0=unlimited)'
47
+ def repo(path = nil)
48
+ if options[:help]
49
+ invoke :help, ['repo']
50
+ else
51
+ require_relative 'scan/repo'
52
+ exit_status = Vtk::Commands::Scan::Repo.new(path, options).execute
53
+ exit exit_status
54
+ end
55
+ end
56
+
57
+ desc 'credentials', 'Audit credentials that may need rotation after a security incident'
58
+ method_option :help, aliases: '-h', type: :boolean,
59
+ desc: 'Display usage information'
60
+ method_option :verbose, aliases: '-v', type: :boolean,
61
+ desc: 'Show all checks including clean ones'
62
+ method_option :json, aliases: '-j', type: :boolean,
63
+ desc: 'Output results as JSON'
64
+ def credentials
65
+ if options[:help]
66
+ invoke :help, ['credentials']
67
+ else
68
+ require_relative 'scan/credentials'
69
+ exit_status = Vtk::Commands::Scan::Credentials.new(options).execute
70
+ exit exit_status
71
+ end
72
+ end
35
73
  end
36
74
  end
37
75
  end
data/lib/vtk/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vtk
4
- VERSION = '1.1.0'
4
+ VERSION = '1.2.0'
5
5
  end