Skip to content

Commit

Permalink
Merge pull request #1 from chrisfosterelli/basic-pckg
Browse files Browse the repository at this point in the history
Basic package
  • Loading branch information
chrisfosterelli authored Mar 14, 2017
2 parents 5873aa9 + c2cc5b7 commit acf3ede
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 43 deletions.
66 changes: 66 additions & 0 deletions README.md
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
43 changes: 0 additions & 43 deletions count.js

This file was deleted.

37 changes: 37 additions & 0 deletions package.json
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"
}
36 changes: 36 additions & 0 deletions src/bin/cmd.js
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))
77 changes: 77 additions & 0 deletions src/lib/count.js
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

0 comments on commit acf3ede

Please sign in to comment.