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 +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +75 -0
- data/lib/vtk/commands/scan/README.md +102 -0
- data/lib/vtk/commands/scan/credentials.rb +59 -0
- data/lib/vtk/commands/scan/repo.rb +77 -0
- data/lib/vtk/commands/scan.rb +41 -3
- data/lib/vtk/version.rb +1 -1
- data/scripts/credential-audit.ps1 +620 -0
- data/scripts/credential-audit.sh +535 -0
- data/scripts/shai-hulud-machine-check.ps1 +625 -0
- data/scripts/shai-hulud-repo-check.ps1 +615 -0
- data/scripts/shai-hulud-repo-check.sh +849 -0
- metadata +10 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0585f00ec9fe122942c5a249fd2733e47baa461960cc9d106e2dfb9ac13dc3aa'
|
|
4
|
+
data.tar.gz: c23b728a27d519e15812f8b95ffcf7e0aac20981e38cab59dca62c4a62582f21
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/vtk/commands/scan.rb
CHANGED
|
@@ -29,9 +29,47 @@ module Vtk
|
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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