Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add knownVulnerable semver range option #30

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ All options are optional.
* `npmVersion` - String, default `null`. The npm version to use when
checking manifest for `engines` requirement satisfaction. (If `null`,
then this particular check is skipped.)
* `avoid` - String, default `null`. A SemVer range of
versions that should be avoided. An avoided version MAY be selected if
there is no other option, so when using this for version selection ensure
that you check the result against the range to see if there was no
alternative available.

### Algorithm

Expand All @@ -114,17 +119,19 @@ All options are optional.
`before` setting, then select that manifest.
6. If nothing is yet selected, sort by the following heuristics in order,
and select the top item:
1. Prioritize versions that are not in `policyRestrictions` over those
1. Prioritize versions that are not in the `avoid` range over those
that are.
2. Prioritize published versions over staged versions.
3. Prioritize versions that are not deprecated, and which have a
2. Prioritize versions that are not in `policyRestrictions` over those
that are.
3. Prioritize published versions over staged versions.
4. Prioritize versions that are not deprecated, and which have a
satisfied engines requirement, over those that are either deprecated
or have an engines mismatch.
4. Prioritize versions that have a satisfied engines requirement over
5. Prioritize versions that have a satisfied engines requirement over
those that do not.
5. Prioritize versions that are not are not deprecated (but have a
6. Prioritize versions that are not are not deprecated (but have a
mismatched engines requirement) over those that are deprecated.
6. Prioritize higher SemVer precedence over lower SemVer precedence.
7. Prioritize higher SemVer precedence over lower SemVer precedence.
7. If no manifest was selected, raise an `ETARGET` error.
8. If the selected item is in the `policyRestrictions.versions` list, raise
an `E403` error.
Expand Down
16 changes: 13 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ const pickManifest = (packument, wanted, opts) => {
before = null,
nodeVersion = process.version,
npmVersion = null,
includeStaged = false
includeStaged = false,
avoid = null
} = opts

const { name, time: verTimes } = packument
Expand Down Expand Up @@ -90,11 +91,18 @@ const pickManifest = (packument, wanted, opts) => {
})
}

const avoidSemverOpt = { includePrerelease: true, loose: true }
const shouldAvoid = ver =>
avoid && semver.satisfies(ver, avoid, avoidSemverOpt)

const sortSemverOpt = { loose: true }
const entries = allEntries.filter(([ver, mani]) =>
semver.satisfies(ver, range, { loose: true }))
.sort((a, b) => {
const [vera, mania] = a
const [verb, manib] = b
const notavoida = !shouldAvoid(vera)
const notavoidb = !shouldAvoid(verb)
const notrestra = !restricted[a]
const notrestrb = !restricted[b]
const notstagea = !staged[a]
Expand All @@ -104,18 +112,20 @@ const pickManifest = (packument, wanted, opts) => {
const enginea = engineOk(mania, npmVersion, nodeVersion)
const engineb = engineOk(manib, npmVersion, nodeVersion)
// sort by:
// - not an avoided version
// - not restricted
// - not staged
// - not deprecated and engine ok
// - engine ok
// - not deprecated
// - semver
return (notrestrb - notrestra) ||
return (notavoidb - notavoida) ||
(notrestrb - notrestra) ||
(notstageb - notstagea) ||
((notdeprb && engineb) - (notdepra && enginea)) ||
(engineb - enginea) ||
(notdeprb - notdepra) ||
semver.rcompare(vera, verb, { loose: true })
semver.rcompare(vera, verb, sortSemverOpt)
})

return entries[0] && entries[0][1]
Expand Down
63 changes: 42 additions & 21 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ test('basic carat range selection', t => {
}
const manifest = pickManifest(metadata, '^1.0.0')
t.equal(manifest.version, '1.0.2', 'picked the right manifest using ^')
t.done()
t.end()
})

test('basic tilde range selection', t => {
Expand All @@ -29,7 +29,7 @@ test('basic tilde range selection', t => {
}
const manifest = pickManifest(metadata, '~1.0.0')
t.equal(manifest.version, '1.0.2', 'picked the right manifest using ~')
t.done()
t.end()
})

test('basic mathematical range selection', t => {
Expand All @@ -45,7 +45,7 @@ test('basic mathematical range selection', t => {
t.equal(manifest1.version, '1.0.2', 'picked the right manifest using mathematical range')
const manifest2 = pickManifest(metadata, '=1.0.0')
t.equal(manifest2.version, '1.0.0', 'picked the right manifest using mathematical range')
t.done()
t.end()
})

test('basic version selection', t => {
Expand All @@ -59,7 +59,7 @@ test('basic version selection', t => {
}
const manifest = pickManifest(metadata, '1.0.0')
t.equal(manifest.version, '1.0.0', 'picked the right manifest using specific version')
t.done()
t.end()
})

test('basic tag selection', t => {
Expand All @@ -76,7 +76,7 @@ test('basic tag selection', t => {
}
const manifest = pickManifest(metadata, 'foo')
t.equal(manifest.version, '1.0.1', 'picked the right manifest using tag')
t.done()
t.end()
})

test('errors if a non-registry spec is provided', t => {
Expand All @@ -94,7 +94,7 @@ test('errors if a non-registry spec is provided', t => {
t.throws(() => {
pickManifest(metadata, 'file://foo.tar.gz')
}, /Only tag, version, and range are supported/)
t.done()
t.end()
})

test('skips any invalid version keys', t => {
Expand All @@ -111,7 +111,7 @@ test('skips any invalid version keys', t => {
t.throws(() => {
pickManifest(metadata, '^1.0.1')
}, { code: 'ETARGET' }, 'no matching specs')
t.done()
t.end()
})

test('ETARGET if range does not match anything', t => {
Expand All @@ -125,7 +125,7 @@ test('ETARGET if range does not match anything', t => {
t.throws(() => {
pickManifest(metadata, '^2.1.0')
}, { code: 'ETARGET' }, 'got correct error on match failure')
t.done()
t.end()
})

test('E403 if version is forbidden', t => {
Expand All @@ -144,7 +144,7 @@ test('E403 if version is forbidden', t => {
t.throws(() => {
pickManifest(metadata, '2.1.0')
}, { code: 'E403' }, 'got correct error on match failure')
t.done()
t.end()
})

test('E403 if version is forbidden, provided a minor version', t => {
Expand All @@ -164,7 +164,7 @@ test('E403 if version is forbidden, provided a minor version', t => {
t.throws(() => {
pickManifest(metadata, '2.1')
}, { code: 'E403' }, 'got correct error on match failure')
t.done()
t.end()
})

test('E403 if version is forbidden, provided a major version', t => {
Expand Down Expand Up @@ -194,7 +194,7 @@ test('E403 if version is forbidden, provided a major version', t => {
t.throws(() => {
pickManifest(metadata, 'borked')
}, { code: 'E403' }, 'got correct error on policy restricted dist-tag')
t.done()
t.end()
})

test('if `defaultTag` matches a given range, use it', t => {
Expand Down Expand Up @@ -225,7 +225,7 @@ test('if `defaultTag` matches a given range, use it', t => {
'1.0.0',
'default to `latest`'
)
t.done()
t.end()
})

test('* ranges use `defaultTag` if no versions match', t => {
Expand Down Expand Up @@ -261,7 +261,7 @@ test('* ranges use `defaultTag` if no versions match', t => {
'1.0.0-pre.0',
'defaulted to `latest` when wanted is ""'
)
t.done()
t.end()
})

test('errors if metadata has no versions', t => {
Expand All @@ -271,7 +271,7 @@ test('errors if metadata has no versions', t => {
t.throws(() => {
pickManifest({}, '^1.0.0')
}, { code: 'ENOVERSIONS' })
t.done()
t.end()
})

test('errors if metadata has no versions or restricted versions', t => {
Expand All @@ -281,7 +281,7 @@ test('errors if metadata has no versions or restricted versions', t => {
t.throws(() => {
pickManifest({}, '^1.0.0')
}, { code: 'ENOVERSIONS' })
t.done()
t.end()
})

test('matches even if requested version has spaces', t => {
Expand All @@ -295,7 +295,7 @@ test('matches even if requested version has spaces', t => {
}
const manifest = pickManifest(metadata, ' 1.0.0 ')
t.equal(manifest.version, '1.0.0', 'picked the right manifest even though `wanted` had spaced')
t.done()
t.end()
})

test('matches even if requested version has garbage', t => {
Expand All @@ -309,7 +309,7 @@ test('matches even if requested version has garbage', t => {
}
const manifest = pickManifest(metadata, '== 1.0.0 || foo')
t.equal(manifest.version, '1.0.0', 'picked the right manifest even though `wanted` had garbage')
t.done()
t.end()
})

test('matches skip deprecated versions', t => {
Expand All @@ -323,7 +323,7 @@ test('matches skip deprecated versions', t => {
}
const manifest = pickManifest(metadata, '^1.0.0')
t.equal(manifest.version, '1.0.1', 'picked the right manifest')
t.done()
t.end()
})

test('matches deprecated versions if we have to', t => {
Expand All @@ -337,7 +337,7 @@ test('matches deprecated versions if we have to', t => {
}
const manifest = pickManifest(metadata, '^1.1.0')
t.equal(manifest.version, '1.1.0', 'picked the right manifest')
t.done()
t.end()
})

test('will use deprecated version if no other suitable match', t => {
Expand All @@ -351,7 +351,7 @@ test('will use deprecated version if no other suitable match', t => {
}
const manifest = pickManifest(metadata, '^1.1.0')
t.equal(manifest.version, '1.1.0', 'picked the right manifest')
t.done()
t.end()
})

test('accepts opts.before option to do date-based cutoffs', t => {
Expand Down Expand Up @@ -414,7 +414,7 @@ test('accepts opts.before option to do date-based cutoffs', t => {
before: '2018-01-02'
})
}, /with a date before/, 'range for out-of-range spec fails even if defaultTag avail')
t.done()
t.end()
})

test('prefers versions that satisfy the engines requirement', t => {
Expand Down Expand Up @@ -474,3 +474,24 @@ test('support selecting staged versions if allowed by options', t => {

t.end()
})

test('support excluding avoided version ranges', t => {
const metadata = {
versions: {
'1.0.0': { version: '1.0.0' },
'1.0.1': { version: '1.0.1' },
'1.0.2': { version: '1.0.2' },
'1.0.3': { version: '1.0.3' },
'2.0.0': { version: '2.0.0' }
}
}
const manifest = pickManifest(metadata, '^1.0.0', {
avoid: '>=1.0.3'
})
t.equal(manifest.version, '1.0.2', 'picked the right manifest using ^')
const cannotAvoid = pickManifest(metadata, '^1.0.0', {
avoid: '1.x'
})
t.equal(cannotAvoid.version, '1.0.3', 'could not avoid within semver range')
t.end()
})