package-audit 0.8.1 → 0.9.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/LICENSE.txt +21 -0
- data/README.md +215 -0
- data/docs/example.png +0 -0
- data/lib/package/audit/const/cmd.rb +0 -4
- data/lib/package/audit/npm/node_collection.rb +17 -11
- data/lib/package/audit/npm/package_lock_parser.rb +80 -0
- data/lib/package/audit/npm/semver_range.rb +138 -0
- data/lib/package/audit/npm/vulnerability_finder.rb +129 -30
- data/lib/package/audit/ruby/gem_meta_data.rb +2 -2
- data/lib/package/audit/services/command_parser.rb +1 -1
- data/lib/package/audit/version.rb +1 -1
- metadata +11 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6a9f8b791f93ef77f2eecb40a1d0bacaebf40c94a06e938ef7528f26281ac30b
|
|
4
|
+
data.tar.gz: e9a24f0114518ba97deaa1687ca5c786fee0ba54dd868cbaec48c4d6d183be7b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e106be4632777be3433e572bb60b34a9b8fa4f7e038f4be86b1d9744ffcaf2e709804bcd4ffffd2b001ee82887ea70cbb7234d2df37a4033224f155ac84b4b88
|
|
7
|
+
data.tar.gz: 400eb639d4f16d4671bc10b1e0b76ad7e49f7f3991239c8f3c5a7dbee5138fcc46d3ae819bcb06021d45a3d46e4ca2cff8e755760dda387976b2fd67d8c8aa03
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Vadim Kononov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Package::Audit
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/package-audit)
|
|
4
|
+
[](https://github.com/vkononov/package-audit/blob/main/.github/workflows/test.yml)
|
|
5
|
+
[](https://github.com/vkononov/package-audit/actions/workflows/test.yml)
|
|
6
|
+
[](https://github.com/vkononov/package-audit/actions/workflows/lint.yml)
|
|
7
|
+
[](https://opensource.org/license/MIT)
|
|
8
|
+
|
|
9
|
+
A useful tool for patch management and prioritization, `package-audit` produces a list of dependencies that are outdated, deprecated or have security vulnerabilities.
|
|
10
|
+
|
|
11
|
+
Supports **Ruby 2.6 → latest**, with the full Ruby matrix tested daily in CI.
|
|
12
|
+
|
|
13
|
+
[](https://www.buymeacoffee.com/vkononov)
|
|
14
|
+
|
|
15
|
+
## Supported Technologies
|
|
16
|
+
|
|
17
|
+
* Ruby
|
|
18
|
+
* Node (using npm or Yarn)
|
|
19
|
+
|
|
20
|
+
## Report Example
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
### Understanding the Flags Column
|
|
25
|
+
|
|
26
|
+
The Flags column shows which risk types apply to each package:
|
|
27
|
+
|
|
28
|
+
- `⦗V··⦘` - Vulnerable (has security vulnerabilities)
|
|
29
|
+
- `⦗·O·⦘` - Outdated (newer version available)
|
|
30
|
+
- `⦗··D⦘` - Deprecated (no updates in 2+ years)
|
|
31
|
+
- `⦗VO·⦘` - Both vulnerable and outdated
|
|
32
|
+
- `⦗VOD⦘` - All three risk types apply
|
|
33
|
+
|
|
34
|
+
The footer uses the same notation (⦗V⦘ulnerable, ⦗O⦘utdated, ⦗D⦘eprecated) as a legend.
|
|
35
|
+
|
|
36
|
+
## Continuous Integration
|
|
37
|
+
|
|
38
|
+
This gem provides a return code of `0` to indicate success and `1` to indicate failure. It is specifically designed for seamless integration into continuous integration pipelines.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
Add this line to your application's Gemfile:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
gem 'package-audit', require: false
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
And then execute:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bundle
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or install it yourself as:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
gem install package-audit
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
### Basic Report
|
|
63
|
+
|
|
64
|
+
Run a full audit (`DIR` defaults to current directory):
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
package-audit [DIR]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Filtering by Risk Type
|
|
71
|
+
|
|
72
|
+
Show only specific risk types:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Show only deprecated packages
|
|
76
|
+
package-audit [DIR] --deprecated
|
|
77
|
+
|
|
78
|
+
# Show only outdated packages
|
|
79
|
+
package-audit [DIR] --outdated
|
|
80
|
+
|
|
81
|
+
# Show only vulnerable packages
|
|
82
|
+
package-audit [DIR] --vulnerable
|
|
83
|
+
|
|
84
|
+
# Show packages that are deprecated and/or vulnerable
|
|
85
|
+
package-audit [DIR] --deprecated --vulnerable
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Excluding Risk Types
|
|
89
|
+
|
|
90
|
+
Exclude specific risk types from the report:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Show everything except deprecated packages
|
|
94
|
+
package-audit [DIR] --skip-deprecated
|
|
95
|
+
|
|
96
|
+
# Show everything except outdated packages
|
|
97
|
+
package-audit [DIR] --skip-outdated
|
|
98
|
+
|
|
99
|
+
# Show everything except vulnerable packages
|
|
100
|
+
package-audit [DIR] --skip-vulnerable
|
|
101
|
+
|
|
102
|
+
# Show only vulnerable packages (exclude deprecated and outdated)
|
|
103
|
+
package-audit [DIR] --skip-deprecated --skip-outdated
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Important:** Packages with multiple risk types are handled intelligently. For example, a package that is both outdated and vulnerable will still appear when using `--skip-outdated` because it has a vulnerability.
|
|
107
|
+
|
|
108
|
+
### Scoping Options
|
|
109
|
+
|
|
110
|
+
Filter by technology or group:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Specific technologies
|
|
114
|
+
package-audit [DIR] -t node -t ruby
|
|
115
|
+
package-audit [DIR] --technology node --technology ruby
|
|
116
|
+
|
|
117
|
+
# Specific groups
|
|
118
|
+
package-audit [DIR] -g staging -g production
|
|
119
|
+
package-audit [DIR] --group staging --group production
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Note:** `Package::Audit` will attempt to automatically detect the technologies used by the project.
|
|
123
|
+
|
|
124
|
+
### Configuration
|
|
125
|
+
|
|
126
|
+
Use a custom configuration file (see [Configuration File](#configuration-file) for details):
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
package-audit [DIR] --config .package-audit.yml
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Include packages normally ignored by configuration:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
package-audit [DIR] --include-ignored
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Output Formats
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# CSV format (-f is an alias for --format)
|
|
142
|
+
package-audit [DIR] --format csv
|
|
143
|
+
|
|
144
|
+
# Markdown format
|
|
145
|
+
package-audit [DIR] --format md
|
|
146
|
+
|
|
147
|
+
# CSV without the header row
|
|
148
|
+
package-audit [DIR] --format csv --exclude-headers
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Other Commands
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# Show how risk is calculated
|
|
155
|
+
package-audit risk
|
|
156
|
+
|
|
157
|
+
# Show version (-v and --version also work)
|
|
158
|
+
package-audit version
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Getting Help
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
package-audit help
|
|
165
|
+
package-audit help [COMMAND]
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Configuration File
|
|
169
|
+
|
|
170
|
+
The `package-audit` gem automatically searches for `.package-audit.yml` in the current directory or in the specified `DIR` if available. However, you have the option to override the default configuration file location by using the `--config` (or `-c`) flag.
|
|
171
|
+
|
|
172
|
+
#### Below is an example of a configuration file:
|
|
173
|
+
|
|
174
|
+
```YAML
|
|
175
|
+
technology:
|
|
176
|
+
node:
|
|
177
|
+
nth-check:
|
|
178
|
+
version: 1.0.2
|
|
179
|
+
vulnerable: false
|
|
180
|
+
ruby:
|
|
181
|
+
devise-async:
|
|
182
|
+
version: 1.0.0
|
|
183
|
+
deprecated: false
|
|
184
|
+
puma:
|
|
185
|
+
version: 6.3.0
|
|
186
|
+
deprecated: false
|
|
187
|
+
selenium-webdriver:
|
|
188
|
+
version: 4.1.0
|
|
189
|
+
outdated: false
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
#### This configuration file allows you to specify the following exclusions:
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
* Ignore all security vulnerabilities associated with `nth-check@1.0.2`.
|
|
196
|
+
* Suppress messages regarding potential deprecations for `device-async@1.0.0` and `puma@6.3.0`.
|
|
197
|
+
* Disable warnings about newer available versions of `selenium-webdriver@4.1.0`
|
|
198
|
+
|
|
199
|
+
**Note:** If the installed package version differs from the expected package version specified in the configuration file, the exclusion settings will not apply to that particular package.
|
|
200
|
+
|
|
201
|
+
**Note:** If a package is reported for multiple reasons (e.g. vulnerable and outdated), it will still be reported unless the exclusion criteria match every reason for being on the report.
|
|
202
|
+
|
|
203
|
+
> By design, wildcard (`*`) version exclusions are not supported to prevent developers from inadvertently overlooking crucial messages when packages are updated.
|
|
204
|
+
|
|
205
|
+
## Contributing
|
|
206
|
+
|
|
207
|
+
1. Fork it
|
|
208
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
209
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
210
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
211
|
+
5. Create new Pull Request
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/license/MIT).
|
data/docs/example.png
ADDED
|
Binary file
|
|
@@ -6,10 +6,6 @@ module Package
|
|
|
6
6
|
BUNDLE_AUDIT_JSON = 'bundle-audit check --update --quiet --format json "%s" 2>/dev/null'
|
|
7
7
|
|
|
8
8
|
NPM_AUDIT = 'npm audit'
|
|
9
|
-
NPM_AUDIT_JSON = 'npm audit --json'
|
|
10
|
-
|
|
11
|
-
YARN_AUDIT = 'yarn audit'
|
|
12
|
-
YARN_AUDIT_JSON = 'yarn audit --json --cwd "%s"'
|
|
13
9
|
end
|
|
14
10
|
end
|
|
15
11
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require_relative '../const/file'
|
|
2
2
|
require_relative '../services/duplicate_package_merger'
|
|
3
3
|
require_relative 'npm_meta_data'
|
|
4
|
+
require_relative 'package_lock_parser'
|
|
4
5
|
require_relative 'vulnerability_finder'
|
|
5
6
|
require_relative 'yarn_lock_parser'
|
|
6
7
|
|
|
@@ -73,22 +74,27 @@ module Package
|
|
|
73
74
|
dependencies.reject { |_name, version| local_dependency?(version) }
|
|
74
75
|
end
|
|
75
76
|
|
|
77
|
+
# Packages that live in the project itself (paths, workspaces, portals)
|
|
78
|
+
# are not published to the registry and cannot be audited against it.
|
|
76
79
|
def local_dependency?(version)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
version.to_s.include?('file:') ||
|
|
80
|
-
# Check for git repositories with local paths
|
|
81
|
-
(version.to_s.start_with?('git+') && version.to_s.include?('file:'))
|
|
80
|
+
version.to_s.start_with?('file:', 'link:', 'workspace:', 'portal:', './', '../') ||
|
|
81
|
+
version.to_s.include?('file:')
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
def fetch_from_lock_file
|
|
85
85
|
default_deps, dev_deps, resolutions = fetch_from_package_json
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
parser = lock_file_parser
|
|
87
|
+
return [] if parser.nil?
|
|
88
|
+
|
|
89
|
+
parser.fetch(default_deps || {}, dev_deps || {}, resolutions || {})
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def lock_file_parser
|
|
93
|
+
yarn_lock = "#{@dir}/#{Const::File::YARN_LOCK}"
|
|
94
|
+
return YarnLockParser.new(yarn_lock) if File.exist?(yarn_lock)
|
|
95
|
+
|
|
96
|
+
package_lock = "#{@dir}/#{Const::File::PACKAGE_LOCK_JSON}"
|
|
97
|
+
PackageLockParser.new(package_lock) if File.exist?(package_lock)
|
|
92
98
|
end
|
|
93
99
|
end
|
|
94
100
|
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
require_relative '../enum/group'
|
|
4
|
+
require_relative '../models/package'
|
|
5
|
+
|
|
6
|
+
# Ruby 2.7 added this. A missing lockfile entry raises it, same as the yarn parser.
|
|
7
|
+
Object.const_set(:NoMatchingPatternError, Class.new(StandardError)) unless defined?(NoMatchingPatternError)
|
|
8
|
+
|
|
9
|
+
module Package
|
|
10
|
+
module Audit
|
|
11
|
+
module Npm
|
|
12
|
+
# Reads package-lock.json (lockfileVersion 1, 2 and 3) and resolves the
|
|
13
|
+
# installed version of each direct dependency.
|
|
14
|
+
class PackageLockParser
|
|
15
|
+
NODE_MODULES = 'node_modules/'
|
|
16
|
+
|
|
17
|
+
def initialize(package_lock_path)
|
|
18
|
+
@package_lock_path = package_lock_path
|
|
19
|
+
@lock = JSON.parse(File.read(package_lock_path))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def fetch(default_deps, dev_deps, _resolutions = {})
|
|
23
|
+
default_deps.merge(dev_deps).map do |dep_name, _expected_version|
|
|
24
|
+
version = direct_version(dep_name)
|
|
25
|
+
raise NoMatchingPatternError, "Unable to find \"#{dep_name}\" in #{@package_lock_path}" if version.nil?
|
|
26
|
+
|
|
27
|
+
pkg = Package.new(dep_name.to_s, version, 'node')
|
|
28
|
+
pkg.update groups: package_groups(dep_name, dev_deps)
|
|
29
|
+
pkg
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Every installed package, including transitive ones, as [name, version] pairs.
|
|
34
|
+
# Entries outside node_modules are workspace sources, not registry packages.
|
|
35
|
+
def all_packages
|
|
36
|
+
if @lock.key?('packages')
|
|
37
|
+
@lock['packages'].map do |path, entry|
|
|
38
|
+
next if entry['version'].nil? || !path.include?(NODE_MODULES)
|
|
39
|
+
|
|
40
|
+
[name_from_path(path), entry['version']]
|
|
41
|
+
end.compact
|
|
42
|
+
else
|
|
43
|
+
walk_v1(@lock['dependencies'] || {})
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def direct_version(dep_name)
|
|
50
|
+
if @lock.key?('packages')
|
|
51
|
+
@lock.dig('packages', "#{NODE_MODULES}#{dep_name}", 'version')
|
|
52
|
+
else
|
|
53
|
+
@lock.dig('dependencies', dep_name, 'version')
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def package_groups(dep_name, dev_deps)
|
|
58
|
+
if dev_deps.key?(dep_name)
|
|
59
|
+
[Enum::Group::DEV]
|
|
60
|
+
else
|
|
61
|
+
[Enum::Group::DEFAULT, Enum::Group::DEV]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# "node_modules/a/node_modules/@scope/b" => "@scope/b"
|
|
66
|
+
def name_from_path(path)
|
|
67
|
+
path.split(NODE_MODULES).last
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def walk_v1(dependencies, packages = [])
|
|
71
|
+
dependencies.each do |name, entry|
|
|
72
|
+
packages << [name, entry['version']] unless entry['version'].nil?
|
|
73
|
+
walk_v1(entry['dependencies'] || {}, packages)
|
|
74
|
+
end
|
|
75
|
+
packages
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
module Package
|
|
2
|
+
module Audit
|
|
3
|
+
module Npm
|
|
4
|
+
# Evaluates npm semver ranges (as used in registry advisories) against an
|
|
5
|
+
# installed version. Supports comparator sets joined by "||", hyphen
|
|
6
|
+
# ranges, caret and tilde ranges, and x-ranges. A range that cannot be
|
|
7
|
+
# understood is treated as matching, so an advisory is never silently
|
|
8
|
+
# dropped because of unusual syntax.
|
|
9
|
+
class SemverRange
|
|
10
|
+
WILDCARDS = %w[* x X].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(range)
|
|
13
|
+
@range = range.to_s.strip
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def satisfied_by?(version)
|
|
17
|
+
return true if @range.empty?
|
|
18
|
+
|
|
19
|
+
installed = parse_version(version)
|
|
20
|
+
return false if installed.nil?
|
|
21
|
+
|
|
22
|
+
@range.split('||').any? { |set| comparator_set_matches?(set.strip, installed) }
|
|
23
|
+
rescue ArgumentError
|
|
24
|
+
true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def comparator_set_matches?(set, installed)
|
|
30
|
+
return true if set.empty? || WILDCARDS.include?(set)
|
|
31
|
+
|
|
32
|
+
Gem::Requirement.new(*requirements_for(set)).satisfied_by?(installed)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def requirements_for(set)
|
|
36
|
+
return hyphen_requirements(*set.split(/\s+-\s+/, 2)) if set.match?(/\s-\s/)
|
|
37
|
+
|
|
38
|
+
set.split(/\s+/).flat_map { |comparator| comparator_requirements(comparator) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def comparator_requirements(comparator)
|
|
42
|
+
case comparator
|
|
43
|
+
when /\A\^(.+)\z/ then caret_requirements(Regexp.last_match(1))
|
|
44
|
+
when /\A~>?(.+)\z/ then tilde_requirements(Regexp.last_match(1))
|
|
45
|
+
when /\A(>=|<=|>|<|=)\s*(.+)\z/ then operator_requirements(Regexp.last_match(1), Regexp.last_match(2))
|
|
46
|
+
else x_range_requirements(comparator)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def operator_requirements(operator, partial)
|
|
51
|
+
parts = version_parts(partial)
|
|
52
|
+
return [] if parts.empty?
|
|
53
|
+
return ["#{operator} #{normalize(partial)}"] if parts.length == 3 || partial.include?('-')
|
|
54
|
+
|
|
55
|
+
partial_operator_requirements(operator, fill(parts), bump(parts))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# ">=1.2" means >=1.2.0, ">1.2" means >=1.3.0, "<=1.2" means <1.3.0, "=1.2" means 1.2.x
|
|
59
|
+
def partial_operator_requirements(operator, lower, upper)
|
|
60
|
+
case operator
|
|
61
|
+
when '>' then [">= #{upper}"]
|
|
62
|
+
when '<=' then ["< #{upper}"]
|
|
63
|
+
when '=' then [">= #{lower}", "< #{upper}"]
|
|
64
|
+
else ["#{operator} #{lower}"]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def hyphen_requirements(low, high)
|
|
69
|
+
requirements = []
|
|
70
|
+
requirements << ">= #{normalize(low)}" unless version_parts(low).empty?
|
|
71
|
+
high_parts = version_parts(high)
|
|
72
|
+
return requirements if high_parts.empty?
|
|
73
|
+
|
|
74
|
+
requirements << (high_parts.length == 3 ? "<= #{normalize(high)}" : "< #{bump(high_parts)}")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def caret_requirements(partial)
|
|
78
|
+
parts = version_parts(partial)
|
|
79
|
+
return [] if parts.empty?
|
|
80
|
+
|
|
81
|
+
# Bump the leftmost non-zero part, or the last given part when all are zero.
|
|
82
|
+
index = parts.index(&:positive?) || (parts.length - 1)
|
|
83
|
+
[">= #{normalize(partial)}", "< #{bump(parts.first(index + 1))}"]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def tilde_requirements(partial)
|
|
87
|
+
parts = version_parts(partial)
|
|
88
|
+
return [] if parts.empty?
|
|
89
|
+
|
|
90
|
+
upper = parts.length == 1 ? "#{parts[0] + 1}.0.0" : "#{parts[0]}.#{parts[1] + 1}.0"
|
|
91
|
+
[">= #{normalize(partial)}", "< #{upper}"]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def x_range_requirements(partial)
|
|
95
|
+
parts = version_parts(partial)
|
|
96
|
+
return [] if parts.empty?
|
|
97
|
+
return ["= #{normalize(partial)}"] if parts.length == 3 && !partial.match?(/[xX*]/)
|
|
98
|
+
|
|
99
|
+
[">= #{fill(parts)}", "< #{bump(parts)}"]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Numeric leading parts of a version, stopping at the first wildcard.
|
|
103
|
+
# "1.2.x" => [1, 2], "1.2.3-beta" => [1, 2, 3], "x" => []
|
|
104
|
+
def version_parts(partial)
|
|
105
|
+
partial.to_s.sub(/\A[v=]+/, '').sub(/[-+].*\z/, '').split('.').take_while do |part|
|
|
106
|
+
part.match?(/\A\d+\z/)
|
|
107
|
+
end.map(&:to_i)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def fill(parts)
|
|
111
|
+
(parts + [0, 0, 0]).first(3).join('.')
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def bump(parts)
|
|
115
|
+
bumped = parts.dup
|
|
116
|
+
bumped[-1] += 1
|
|
117
|
+
fill(bumped)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Gem::Version understands "1.2.3-beta.1" (as a prerelease) but not build
|
|
121
|
+
# metadata, a leading "v", or missing patch numbers.
|
|
122
|
+
def normalize(version)
|
|
123
|
+
cleaned = version.to_s.sub(/\A[v=]+/, '').sub(/\+.*\z/, '')
|
|
124
|
+
numeric, prerelease = cleaned.split('-', 2)
|
|
125
|
+
numeric = numeric.to_s
|
|
126
|
+
numeric = fill(numeric.split('.').map(&:to_i)) if numeric.match?(/\A\d+(\.\d+){0,2}\z/)
|
|
127
|
+
prerelease ? "#{numeric}-#{prerelease}" : numeric
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def parse_version(version)
|
|
131
|
+
Gem::Version.new(normalize(version))
|
|
132
|
+
rescue ArgumentError
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'net/http'
|
|
3
|
+
require 'openssl'
|
|
2
4
|
|
|
3
|
-
require_relative '../const/
|
|
5
|
+
require_relative '../const/file'
|
|
4
6
|
require_relative '../enum/vulnerability_type'
|
|
7
|
+
require_relative 'package_lock_parser'
|
|
8
|
+
require_relative 'semver_range'
|
|
5
9
|
|
|
6
10
|
module Package
|
|
7
11
|
module Audit
|
|
8
12
|
module Npm
|
|
9
|
-
class VulnerabilityFinder
|
|
10
|
-
|
|
13
|
+
class VulnerabilityFinder # rubocop:disable Metrics/ClassLength
|
|
14
|
+
BULK_ADVISORY_URL = 'https://registry.npmjs.org/-/npm/v1/security/advisories/bulk'
|
|
15
|
+
BATCH_SIZE = 100
|
|
16
|
+
TIMEOUT = 30
|
|
17
|
+
VERSION_LINE = /^\s+"?version"?\s+"([^"]+)"/
|
|
11
18
|
|
|
12
19
|
def initialize(dir, pkgs)
|
|
13
20
|
@dir = dir
|
|
@@ -16,48 +23,140 @@ module Package
|
|
|
16
23
|
end
|
|
17
24
|
|
|
18
25
|
def run
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
env_vars = { 'NODE_NO_WARNINGS' => '1' }
|
|
26
|
+
versions_by_name = collect_versions_by_name
|
|
27
|
+
return [] if versions_by_name.empty?
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
versions_by_name.each_slice(BATCH_SIZE) do |batch|
|
|
30
|
+
batch_versions = batch.to_h
|
|
31
|
+
fetch_advisories(batch_versions).each do |name, advisories|
|
|
32
|
+
package_name = name.to_s
|
|
33
|
+
apply_advisories(package_name, Array(advisories), batch_versions[package_name] || [])
|
|
34
|
+
end
|
|
29
35
|
end
|
|
36
|
+
|
|
30
37
|
@vuln_hash.values
|
|
31
38
|
end
|
|
32
39
|
|
|
33
40
|
private
|
|
34
41
|
|
|
35
|
-
def
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
def collect_versions_by_name
|
|
43
|
+
versions_by_name = {}
|
|
44
|
+
|
|
45
|
+
(lockfile_packages + @pkg_hash.values.map { |pkg| [pkg.name, pkg.version] }).each do |name, version|
|
|
46
|
+
next if version.to_s.empty? || version == 'unknown'
|
|
47
|
+
|
|
48
|
+
versions = versions_by_name[name] ||= []
|
|
49
|
+
versions << version unless versions.include?(version)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
versions_by_name
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def lockfile_packages
|
|
56
|
+
yarn_lock = "#{@dir}/#{Const::File::YARN_LOCK}"
|
|
57
|
+
return yarn_lockfile_packages(yarn_lock) if File.exist?(yarn_lock)
|
|
58
|
+
|
|
59
|
+
package_lock = "#{@dir}/#{Const::File::PACKAGE_LOCK_JSON}"
|
|
60
|
+
return PackageLockParser.new(package_lock).all_packages if File.exist?(package_lock)
|
|
38
61
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
62
|
+
[]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def yarn_lockfile_packages(path)
|
|
66
|
+
packages = []
|
|
67
|
+
current_names = []
|
|
68
|
+
File.foreach(path) do |line|
|
|
69
|
+
current_names, packages = process_lockfile_line(line, current_names, packages)
|
|
70
|
+
end
|
|
71
|
+
packages
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def process_lockfile_line(line, current_names, packages)
|
|
75
|
+
if lockfile_key_line?(line)
|
|
76
|
+
[package_names_from_lock_key(line.strip.chomp(':')), packages]
|
|
77
|
+
elsif (match = line.match(VERSION_LINE))
|
|
78
|
+
match_version = match[1]
|
|
79
|
+
current_names.each { |name| packages << [name, match_version] }
|
|
80
|
+
[[], packages]
|
|
81
|
+
else
|
|
82
|
+
[current_names, packages]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def lockfile_key_line?(line)
|
|
87
|
+
line.match?(/^\S/) && line.include?('@') && line.strip.end_with?(':')
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def package_names_from_lock_key(key)
|
|
91
|
+
key.split(',').map do |entry|
|
|
92
|
+
package_name_from_spec(entry.strip.gsub(/\A["']|["']\z/, ''))
|
|
93
|
+
end.uniq
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def package_name_from_spec(spec)
|
|
97
|
+
if spec.start_with?('@')
|
|
98
|
+
"@#{spec.split('@', 3)[1]}"
|
|
99
|
+
else
|
|
100
|
+
spec.split('@', 2).first
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def fetch_advisories(versions_by_name)
|
|
105
|
+
uri = URI(BULK_ADVISORY_URL)
|
|
106
|
+
response = make_request(uri, versions_by_name.to_json)
|
|
107
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
108
|
+
warn "Warning: Unable to fetch npm advisories (#{response.code} #{response.message})."
|
|
109
|
+
return {}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
JSON.parse(response.body)
|
|
113
|
+
rescue StandardError => e
|
|
114
|
+
warn "Warning: Error while fetching npm advisories: #{e.message}"
|
|
115
|
+
{}
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def make_request(uri, body)
|
|
119
|
+
request = Net::HTTP::Post.new(uri)
|
|
120
|
+
request['Content-Type'] = 'application/json'
|
|
121
|
+
request['Accept'] = 'application/json'
|
|
122
|
+
request.body = body
|
|
123
|
+
create_http_client(uri).request(request)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def create_http_client(uri)
|
|
127
|
+
Net::HTTP.new(uri.host, uri.port).tap do |http|
|
|
128
|
+
http.use_ssl = true
|
|
129
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
130
|
+
http.cert_store = OpenSSL::X509::Store.new
|
|
131
|
+
http.cert_store.set_default_paths
|
|
132
|
+
http.read_timeout = TIMEOUT
|
|
133
|
+
http.open_timeout = TIMEOUT
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def apply_advisories(name, advisories, versions)
|
|
138
|
+
advisories.each do |advisory|
|
|
139
|
+
Array(versions).each do |version|
|
|
140
|
+
next unless version_matches?(version, advisory['vulnerable_versions'])
|
|
141
|
+
|
|
142
|
+
update_vulnerability_info(name, version, advisory['severity'])
|
|
143
|
+
end
|
|
144
|
+
end
|
|
43
145
|
end
|
|
44
146
|
|
|
45
|
-
def
|
|
46
|
-
|
|
47
|
-
name: advisory[:module_name],
|
|
48
|
-
version: advisory[:findings][0][:version]
|
|
49
|
-
}
|
|
147
|
+
def version_matches?(version, vulnerable_versions)
|
|
148
|
+
SemverRange.new(vulnerable_versions).satisfied_by?(version)
|
|
50
149
|
end
|
|
51
150
|
|
|
52
|
-
def update_vulnerability_info(
|
|
53
|
-
name = package_info[:name]
|
|
54
|
-
version = package_info[:version]
|
|
151
|
+
def update_vulnerability_info(name, version, severity)
|
|
55
152
|
full_name = "#{name}@#{version}"
|
|
56
153
|
vulnerability = severity || Enum::VulnerabilityType::UNKNOWN
|
|
57
154
|
|
|
58
155
|
@vuln_hash[full_name] ||= Package.new(name, version, 'node')
|
|
59
|
-
@vuln_hash[full_name].update
|
|
60
|
-
|
|
156
|
+
@vuln_hash[full_name].update(
|
|
157
|
+
vulnerabilities: @vuln_hash[full_name].vulnerabilities + [vulnerability],
|
|
158
|
+
groups: @pkg_hash[name]&.groups&.map(&:to_s) || []
|
|
159
|
+
)
|
|
61
160
|
end
|
|
62
161
|
end
|
|
63
162
|
end
|
|
@@ -226,8 +226,8 @@ module Package
|
|
|
226
226
|
|
|
227
227
|
def parse_version_dates(response_body)
|
|
228
228
|
versions = JSON.parse(response_body)
|
|
229
|
-
versions.
|
|
230
|
-
|
|
229
|
+
versions.to_h do |version_info|
|
|
230
|
+
[version_info['number'], version_info['created_at']]
|
|
231
231
|
end
|
|
232
232
|
end
|
|
233
233
|
|
|
@@ -140,7 +140,7 @@ module Package
|
|
|
140
140
|
when Enum::Technology::RUBY
|
|
141
141
|
Const::Cmd::BUNDLE_AUDIT
|
|
142
142
|
when Enum::Technology::NODE
|
|
143
|
-
Const::Cmd::
|
|
143
|
+
Const::Cmd::NPM_AUDIT
|
|
144
144
|
else
|
|
145
145
|
raise ArgumentError, "Unexpected technology \"#{technology}\" found in #{__method__}"
|
|
146
146
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: package-audit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vadim Kononov
|
|
@@ -37,8 +37,9 @@ dependencies:
|
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '1.2'
|
|
40
|
-
description:
|
|
41
|
-
|
|
40
|
+
description: Audits project dependencies across supported package managers and lists
|
|
41
|
+
those that are outdated, deprecated or have known security vulnerabilities, for
|
|
42
|
+
patch prioritization.
|
|
42
43
|
email:
|
|
43
44
|
- vadim@konoson.com
|
|
44
45
|
executables:
|
|
@@ -46,6 +47,9 @@ executables:
|
|
|
46
47
|
extensions: []
|
|
47
48
|
extra_rdoc_files: []
|
|
48
49
|
files:
|
|
50
|
+
- LICENSE.txt
|
|
51
|
+
- README.md
|
|
52
|
+
- docs/example.png
|
|
49
53
|
- exe/package-audit
|
|
50
54
|
- lib/package/audit/cli.rb
|
|
51
55
|
- lib/package/audit/const/cmd.rb
|
|
@@ -70,6 +74,8 @@ files:
|
|
|
70
74
|
- lib/package/audit/models/risk.rb
|
|
71
75
|
- lib/package/audit/npm/node_collection.rb
|
|
72
76
|
- lib/package/audit/npm/npm_meta_data.rb
|
|
77
|
+
- lib/package/audit/npm/package_lock_parser.rb
|
|
78
|
+
- lib/package/audit/npm/semver_range.rb
|
|
73
79
|
- lib/package/audit/npm/vulnerability_finder.rb
|
|
74
80
|
- lib/package/audit/npm/yarn_lock_parser.rb
|
|
75
81
|
- lib/package/audit/ruby/bundler_specs.rb
|
|
@@ -111,7 +117,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
111
117
|
- !ruby/object:Gem::Version
|
|
112
118
|
version: '0'
|
|
113
119
|
requirements: []
|
|
114
|
-
rubygems_version:
|
|
120
|
+
rubygems_version: 4.0.10
|
|
115
121
|
specification_version: 4
|
|
116
|
-
summary:
|
|
122
|
+
summary: CLI that reports outdated, deprecated and vulnerable dependencies in a project
|
|
117
123
|
test_files: []
|