-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from chrisfosterelli/basic-pckg
Basic package
- Loading branch information
Showing
5 changed files
with
216 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
NPM Contributor Count | ||
===================== | ||
|
||
This is a tool that counts the number of people who have access to a project on | ||
npm, either directly or indirectly. Every single module you import, or any | ||
module that those modules import (and so on), can put your project at risk so | ||
it is important to understand the exposure you are receiving when you add an npm | ||
package as a dependency to your project. I've writte a bit about this [here]. | ||
|
||
This package requires a minimum of Node 6, as many ES6 features are used. | ||
|
||
Installing | ||
---------- | ||
|
||
```bash | ||
npm install -g contributor-count | ||
``` | ||
|
||
Usage | ||
----- | ||
|
||
To test all of the dependencies for the package in your local directory: | ||
|
||
```bash | ||
> contributor-count | ||
local package has 19 contributors with access to the project or its dependencies | ||
> | ||
``` | ||
|
||
To test all of the dependencies for a given package on npm: | ||
|
||
```bash | ||
> contributor-count express | ||
express has 51 contributors with access to the project or its dependencies | ||
> | ||
``` | ||
|
||
You can also use it as a library: | ||
|
||
```javascript | ||
const count = require('contributor-count') | ||
|
||
const name = 'express' | ||
const version = 'latest' | ||
const target = { name, version } | ||
|
||
count(target) | ||
.then(count => console.log('The count is', count)) | ||
.catch(err => console.error('Failed to get count', err)) | ||
``` | ||
|
||
Technical Notes | ||
--------------- | ||
|
||
This counter does not consider packages which reference off-npm sources, such | ||
as a direct git repository or a tar file since it's not clear how to quantify | ||
the number of people with access to these reliably. | ||
|
||
This counter only considers the number of people with direct npm publish rights | ||
for the package or one of its dependencies. It does not consider the possibility | ||
of a vulnerability being introduced through a packages repository or CI system. | ||
|
||
This counter only considers dependencies which are required for usage and does | ||
not include unlisted dependencies, peer dependencies, or developer dependencies. | ||
|
||
[here]: https://fosterelli.co/stealing-credentials-with-a-malicious-node-module.html |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
{ | ||
"name": "contributor-count", | ||
"version": "1.0.0", | ||
"description": "Count the number of people with write access to a package", | ||
"main": "src/lib/count.js", | ||
"bin": { | ||
"contributor-count": "src/bin/cmd.js" | ||
}, | ||
"dependencies": { | ||
"axios": "^0.12.0", | ||
"bluebird": "^3.4.1", | ||
"lodash": "^4.13.1", | ||
"ora": "^1.1.0", | ||
"promise-throttle": "^0.2.0", | ||
"semver": "^5.3.0" | ||
}, | ||
"devDependencies": {}, | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/chrisfosterelli/npm-contributor-count.git" | ||
}, | ||
"keywords": [ | ||
"npm", | ||
"security", | ||
"contributor", | ||
"count" | ||
], | ||
"author": "Chris Foster <[email protected]>", | ||
"license": "MIT", | ||
"bugs": { | ||
"url": "https://github.com/chrisfosterelli/npm-contributor-count/issues" | ||
}, | ||
"homepage": "https://github.com/chrisfosterelli/npm-contributor-count#readme" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
|
||
/* Command line script | ||
*/ | ||
|
||
const ora = require('ora') | ||
const count = require('../lib/count') | ||
|
||
function getLocalPackages() { | ||
const json = require(process.cwd() + '/package.json') | ||
const dependencies = json.dependencies | ||
const keys = Object.keys(dependencies) | ||
return keys.map(name => { | ||
const version = dependencies[name] | ||
return { name, version } | ||
}) | ||
} | ||
|
||
function finish(count, spinner, name = 'local package') { | ||
spinner.stop() | ||
console.log(name, 'has', count, | ||
'contributors with access to the project or its dependencies') | ||
} | ||
|
||
const name = process.argv[2] | ||
const version = process.argv[3] || 'latest' | ||
|
||
const target = name ? | ||
{ name, version } : | ||
getLocalPackages() | ||
|
||
const spinner = ora('Crawling project dependency tree') | ||
spinner.start() | ||
|
||
count(target) | ||
.then(count => finish(count, spinner, target.name)) | ||
.catch(err => console.error(err)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
|
||
/* Npm Contributor Count | ||
* Count how many contributors have access to a npm package | ||
*/ | ||
|
||
const _ = require('lodash') | ||
const axios = require('axios') | ||
const semver = require('semver') | ||
const Promise = require('bluebird') | ||
const Throttle = require('promise-throttle') | ||
|
||
const cache = {} | ||
const visited = {} | ||
|
||
const requestsPerSecond = 10 | ||
const promiseImplementation = Promise | ||
const throttle = new Throttle({ requestsPerSecond, promiseImplementation }) | ||
|
||
function npmContributorCount(pckgs) { | ||
const set = pckgs.length ? pckgs : [ pckgs ] | ||
return fetchMaintainers(set) | ||
.then(contributors => contributors.length) | ||
} | ||
|
||
// Fetch a list of maintainers | ||
// for an array of packages | ||
function fetchMaintainers(pckgs) { | ||
const promises = pckgs.map(pckg => { | ||
return fetchPackage(pckg.name) | ||
.then(metadata => parseMaintainers(metadata, pckg.version)) | ||
}) | ||
return Promise.all(promises) | ||
.then(results => _.flatten(results)) | ||
.then(results => _.uniq(results)) | ||
} | ||
|
||
// Fetch a package's metadata | ||
// Rate limit and cache as needed | ||
function fetchPackage(name) { | ||
const npm = 'https://registry.npmjs.org/' | ||
const url = npm + name.split('@')[0] | ||
if (cache[url]) return Promise.resolve(cache[url]) | ||
return throttle | ||
.add(() => axios.get(url)) | ||
.then(resp => { | ||
cache[url] = resp.data | ||
return resp.data | ||
}) | ||
} | ||
|
||
// Parse the set of maintainers from a package's | ||
// npm metadata for a given target version | ||
function parseMaintainers(metadata, version) { | ||
const key = metadata.name + '@' + version | ||
const target = parseVersion(metadata, version) | ||
const maintainers = metadata.maintainers.map(m => m.name) | ||
if (visited[key]) return [] // Avoid cycling dependencies | ||
else visited[key] = true | ||
if (!target || !target.dependencies) return maintainers | ||
const dependencies = Object.keys(target.dependencies).map(name => { | ||
const version = target.dependencies[name] | ||
return { name, version } | ||
}) | ||
return fetchMaintainers(dependencies) | ||
.then(deepMaintainers => maintainers.concat(deepMaintainers)) | ||
} | ||
|
||
// Parse package metadata for a | ||
// specific version of the package | ||
function parseVersion(metadata, version) { | ||
const all = Object.keys(metadata.versions) | ||
const target = metadata['dist-tags'][version] || version | ||
const resolved = semver.maxSatisfying(all, target) | ||
return metadata.versions[resolved] | ||
} | ||
|
||
module.exports = npmContributorCount |