diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..554c72a --- /dev/null +++ b/.eslintrc @@ -0,0 +1,5 @@ +{ + "root": true, + + "extends": "@ljharb/eslint-config/node/18", +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..7ebfc8c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [ljharb] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: npm/scorecard-cli +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/node-pretest.yml b/.github/workflows/node-pretest.yml new file mode 100644 index 0000000..765edf7 --- /dev/null +++ b/.github/workflows/node-pretest.yml @@ -0,0 +1,7 @@ +name: 'Tests: pretest/posttest' + +on: [pull_request, push] + +jobs: + tests: + uses: ljharb/actions/.github/workflows/pretest.yml@main diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml new file mode 100644 index 0000000..0e0f1db --- /dev/null +++ b/.github/workflows/node.yml @@ -0,0 +1,18 @@ +name: 'Tests: node.js' + +on: [pull_request, push] + +jobs: + tests: + uses: ljharb/actions/.github/workflows/node.yml@main + with: + range: '^18.16.0 || ^20.2.0' + type: minors + command: npm run tests-only + + node: + name: 'node (engines.node)' + needs: [tests] + runs-on: ubuntu-latest + steps: + - run: 'echo tests completed' diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml new file mode 100644 index 0000000..1818191 --- /dev/null +++ b/.github/workflows/rebase.yml @@ -0,0 +1,22 @@ +name: Automatic Rebase + +on: [pull_request_target] + +permissions: + contents: read + +jobs: + _: + permissions: + contents: write # for ljharb/rebase to push code to rebase + pull-requests: read # for ljharb/rebase to get info about PR + + name: "Automatic Rebase" + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: ljharb/rebase@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/require-allow-edits.yml b/.github/workflows/require-allow-edits.yml new file mode 100644 index 0000000..7b842f8 --- /dev/null +++ b/.github/workflows/require-allow-edits.yml @@ -0,0 +1,12 @@ +name: Require “Allow Edits” + +on: [pull_request_target] + +jobs: + _: + name: "Require “Allow Edits”" + + runs-on: ubuntu-latest + + steps: + - uses: ljharb/require-allow-edits@main diff --git a/.nycrc b/.nycrc new file mode 100644 index 0000000..1826526 --- /dev/null +++ b/.nycrc @@ -0,0 +1,13 @@ +{ + "all": true, + "check-coverage": false, + "reporter": ["text-summary", "text", "html", "json"], + "lines": 86, + "statements": 85.93, + "functions": 82.43, + "branches": 76.06, + "exclude": [ + "coverage", + "test" + ] +} diff --git a/README.md b/README.md index d0a3c27..a9f9c9a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,226 @@ -# scorecard-cli +# scorecard-cli [![Version Badge][npm-version-svg]][package-url] + +[![github actions][actions-image]][actions-url] +[![coverage][codecov-image]][codecov-url] +[![License][license-image]][license-url] +[![Downloads][downloads-image]][downloads-url] + +[![npm badge][npm-badge-png]][package-url] + A CLI for OpenSSF Scorecard data. + +## Example + +### CLI + +```console +> scorecard-cli ljharb/qs +{ + date: '2023-05-22', + repo: { + name: 'github.com/ljharb/qs', + commit: '410bdd3c8ae7f5d7ae9b52648b8642b8adc5e1c0' + }, + scorecard: { + version: 'v4.10.5-188-g028fa93', + commit: '028fa93e924d3facde890a113f7edf1225a87ea2' + }, + score: 6.8, + checks: [ + { + name: 'Maintained', + score: 8, + reason: '4 commit(s) out of 30 and 6 issue activity out of 30 found in the last 90 days -- score normalized to 8', + details: null, + documentation: { + short: 'Determines if the project is "actively maintained".', + url: 'https://github.com/ossf/scorecard/blob/028fa93e924d3facde890a113f7edf1225a87ea2/docs/checks.md#maintained' + } + }, + { + name: 'Code-Review', + score: 0, + reason: 'found 26 unreviewed human changesets (30 total)', + details: null, + documentation: { + short: 'Determines if the project requires human code review before pull requests (aka merge requests) are merged.', + url: 'https://github.com/ossf/scorecard/blob/028fa93e924d3facde890a113f7edf1225a87ea2/docs/checks.md#code-review' + } + }, + { + name: 'CII-Best-Practices', + score: 0, + reason: 'no effort to earn an OpenSSF best practices badge detected', + details: null, + documentation: { + short: 'Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.', + url: 'https://github.com/ossf/scorecard/blob/028fa93e924d3facde890a113f7edf1225a87ea2/docs/checks.md#cii-best-practices' + } + }, + { + name: 'License', + score: 10, + reason: 'license file detected', + details: [ + 'Info: License file found in expected location: LICENSE.md:1', + 'Info: FSF or OSI recognized license: LICENSE.md:1' + ], + documentation: { + short: 'Determines if the project has defined a license.', + url: 'https://github.com/ossf/scorecard/blob/028fa93e924d3facde890a113f7edf1225a87ea2/docs/checks.md#license' + } + }, + { + name: 'Branch-Protection', + score: -1, + reason: 'internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration', + details: null, + documentation: { + short: "Determines if the default and release branches are protected with GitHub's branch protection settings.", + url: 'https://github.com/ossf/scorecard/blob/028fa93e924d3facde890a113f7edf1225a87ea2/docs/checks.md#branch-protection' + } + }, + { + name: 'Signed-Releases', + score: -1, + reason: 'no releases found', + details: [ 'Warn: no GitHub releases found' ], + documentation: { + short: 'Determines if the project cryptographically signs release artifacts.', + url: 'https://github.com/ossf/scorecard/blob/028fa93e924d3facde890a113f7edf1225a87ea2/docs/checks.md#signed-releases' + } + }, + { + name: 'Binary-Artifacts', + score: 10, + reason: 'no binaries found in the repo', + details: null, + documentation: { + short: 'Determines if the project has generated executable (binary) artifacts in the source repository.', + url: 'https://github.com/ossf/scorecard/blob/028fa93e924d3facde890a113f7edf1225a87ea2/docs/checks.md#binary-artifacts' + } + }, + { + name: 'Pinned-Dependencies', + score: 8, + reason: 'dependency not pinned by hash detected -- score normalized to 8', + details: [ + 'Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/rebase.yml:19: update your workflow using https://app.stepsecurity.io/secureworkflow/ljharb/qs/rebase.yml/main?enable=pin', + 'Warn: third-party GitHubAction not pinned by hash: .github/workflows/rebase.yml:20: update your workflow using https://app.stepsecurity.io/secureworkflow/ljharb/qs/rebase.yml/main?enable=pin', + 'Warn: third-party GitHubAction not pinned by hash: .github/workflows/require-allow-edits.yml:18: update your workflow using https://app.stepsecurity.io/secureworkflow/ljharb/qs/require-allow-edits.yml/main?enable=pin', + 'Info: Dockerfile dependencies are pinned', + 'Info: no insecure (not pinned by hash) dependency downloads found in Dockerfiles', + 'Info: no insecure (not pinned by hash) dependency downloads found in shell scripts', + 'Info: Pip installs are pinned' + ], + documentation: { + short: 'Determines if the project has declared and pinned the dependencies of its build process.', + url: 'https://github.com/ossf/scorecard/blob/028fa93e924d3facde890a113f7edf1225a87ea2/docs/checks.md#pinned-dependencies' + } + }, + { + name: 'Dangerous-Workflow', + score: 10, + reason: 'no dangerous workflow patterns detected', + details: null, + documentation: { + short: "Determines if the project's GitHub Action workflows avoid dangerous patterns.", + url: 'https://github.com/ossf/scorecard/blob/028fa93e924d3facde890a113f7edf1225a87ea2/docs/checks.md#dangerous-workflow' + } + }, + { + name: 'Packaging', + score: -1, + reason: 'no published package detected', + details: [ 'Warn: no GitHub/GitLab publishing workflow detected' ], + documentation: { + short: 'Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.', + url: 'https://github.com/ossf/scorecard/blob/028fa93e924d3facde890a113f7edf1225a87ea2/docs/checks.md#packaging' + } + }, + { + name: 'Token-Permissions', + score: 10, + reason: 'tokens are read-only in GitHub workflows', + details: [ + "Info: topLevel 'contents' permission set to 'read': .github/workflows/node-aught.yml:6", + "Info: topLevel 'contents' permission set to 'read': .github/workflows/node-pretest.yml:6", + "Info: topLevel 'contents' permission set to 'read': .github/workflows/node-tens.yml:6", + "Info: topLevel 'contents' permission set to 'read': .github/workflows/rebase.yml:6", + "Warn: jobLevel 'contents' permission set to 'write': .github/workflows/rebase.yml:11: Verify which permissions are needed and consider whether you can reduce them. (High effort)", + "Info: jobLevel 'pull-requests' permission set to 'read': .github/workflows/rebase.yml:12", + "Info: topLevel 'contents' permission set to 'read': .github/workflows/require-allow-edits.yml:6", + "Info: jobLevel 'pull-requests' permission set to 'read': .github/workflows/require-allow-edits.yml:11" + ], + documentation: { + short: "Determines if the project's workflows follow the principle of least privilege.", + url: 'https://github.com/ossf/scorecard/blob/028fa93e924d3facde890a113f7edf1225a87ea2/docs/checks.md#token-permissions' + } + }, + { + name: 'Vulnerabilities', + score: 10, + reason: 'no vulnerabilities detected', + details: null, + documentation: { + short: 'Determines if the project has open, known unfixed vulnerabilities.', + url: 'https://github.com/ossf/scorecard/blob/028fa93e924d3facde890a113f7edf1225a87ea2/docs/checks.md#vulnerabilities' + } + }, + { + name: 'Fuzzing', + score: 0, + reason: 'project is not fuzzed', + details: null, + documentation: { + short: 'Determines if the project uses fuzzing.', + url: 'https://github.com/ossf/scorecard/blob/028fa93e924d3facde890a113f7edf1225a87ea2/docs/checks.md#fuzzing' + } + }, + { + name: 'Security-Policy', + score: 9, + reason: 'security policy file detected', + details: [ + 'Info: Found linked content in security policy: github.com/ljharb/.github/SECURITY.md', + 'Info: Found text in security policy: github.com/ljharb/.github/SECURITY.md', + 'Warn: One or no descriptive hints of disclosure, vulnerability, and/or timelines in security policy: github.com/ljharb/.github/SECURITY.md', + 'Info: security policy detected in org repo: github.com/ljharb/.github/SECURITY.md' + ], + documentation: { + short: 'Determines if the project has published a security policy.', + url: 'https://github.com/ossf/scorecard/blob/028fa93e924d3facde890a113f7edf1225a87ea2/docs/checks.md#security-policy' + } + }, + { + name: 'SAST', + score: 0, + reason: 'SAST tool is not run on all commits -- score normalized to 0', + details: [ + 'Warn: 0 commits out of 5 are checked with a SAST tool', + 'Warn: CodeQL tool not detected' + ], + documentation: { + short: 'Determines if the project uses static code analysis.', + url: 'https://github.com/ossf/scorecard/blob/028fa93e924d3facde890a113f7edf1225a87ea2/docs/checks.md#sast' + } + } + ] +} +``` + +[package-url]: https://npmjs.org/package/scorecard-cli +[npm-version-svg]: https://versionbadg.es/ljharb/scorecard-cli.svg +[deps-svg]: https://david-dm.org/ljharb/scorecard-cli.svg +[deps-url]: https://david-dm.org/ljharb/scorecard-cli +[dev-deps-svg]: https://david-dm.org/ljharb/scorecard-cli/dev-status.svg +[dev-deps-url]: https://david-dm.org/ljharb/scorecard-cli#info=devDependencies +[npm-badge-png]: https://nodei.co/npm/scorecard-cli.png?downloads=true&stars=true +[license-image]: https://img.shields.io/npm/l/scorecard-cli.svg +[license-url]: LICENSE +[downloads-image]: https://img.shields.io/npm/dm/scorecard-cli.svg +[downloads-url]: https://npm-stat.com/charts.html?package=scorecard-cli +[codecov-image]: https://codecov.io/gh/ljharb/scorecard-cli/branch/main/graphs/badge.svg +[codecov-url]: https://app.codecov.io/gh/ljharb/scorecard-cli/ +[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/ljharb/scorecard-cli +[actions-url]: https://github.com/ljharb/scorecard-cli/actions \ No newline at end of file diff --git a/bin/scorecard-cli b/bin/scorecard-cli new file mode 100755 index 0000000..e9ec16f --- /dev/null +++ b/bin/scorecard-cli @@ -0,0 +1,24 @@ +#!/usr/bin/env node + +'use strict'; + +const { inspect, parseArgs } = require('util'); + +const { positionals } = parseArgs({ allowPositionals: true }); + +if (positionals.length !== 1) { + console.error('Expected exactly one positional argument: the repository slug (e.g. "nvm-sh/nvm")'); + process.exit(1); +} + +// module.exports assignment is solely for testing. +module.exports = import('@nodesecure/ossf-scorecard-sdk').then(async ({ result }) => { + const results = await result(positionals[0]); + + const output = inspect(results, { colors: process.env.TERM !== 'dumb', depth: Infinity }); + + console.log(output); +}).catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/package.json b/package.json index 47c319a..dc77455 100644 --- a/package.json +++ b/package.json @@ -6,19 +6,29 @@ "exports": { "./package.json": "./package.json" }, + "bin": "./bin/scorecard-cli", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "prepack": "npmignore --auto --commentLines=autogenerated", + "prepublishOnly": "safe-publish-latest", + "prepublish": "not-in-publish || npm run prepublishOnly", + "lint": "eslint --ext=js,mjs . bin/**/*", + "pretest": "npm run lint", + "tests-only": "nyc tape 'test/**/*.js'", + "test": "npm run tests-only", + "posttest": "aud --production", + "version": "auto-changelog && git add CHANGELOG.md", + "postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\"" }, "repository": { "type": "git", "url": "git+https://github.com/ljharb/scorecard-cli.git" }, "keywords": [ - "openssf", + "OpenSSF", "ossf", "scorecard", - "cli", "scorecards", + "cli", "security" ], "author": "Jordan Harband ", @@ -26,5 +36,36 @@ "bugs": { "url": "https://github.com/ljharb/scorecard-cli/issues" }, - "homepage": "https://github.com/ljharb/scorecard-cli#readme" + "homepage": "https://github.com/ljharb/scorecard-cli#readme", + "dependencies": { + "@nodesecure/ossf-scorecard-sdk": "^1.1.1" + }, + "devDependencies": { + "@ljharb/eslint-config": "^21.1.0", + "aud": "^2.0.2", + "auto-changelog": "^2.4.0", + "eslint": "^8.8.0", + "in-publish": "^2.0.1", + "mock-property": "^1.0.0", + "npmignore": "^0.3.0", + "nyc": "^10.3.2", + "safe-publish-latest": "^2.0.0", + "tape": "^5.6.3" + }, + "engines": { + "node": "^18.16.0 || ^20.2.0" + }, + "auto-changelog": { + "output": "CHANGELOG.md", + "template": "keepachangelog", + "unreleased": false, + "commitLimit": false, + "backfillLimit": false, + "hideCredit": true + }, + "publishConfig": { + "ignore": [ + ".github/workflows" + ] + } } diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..a8f6d32 --- /dev/null +++ b/test/index.js @@ -0,0 +1,30 @@ +'use strict'; + +const test = require('tape'); +const mockProperty = require('mock-property'); + +process.argv = [process.argv[0], process.argv[1], 'nvm-sh/nvm']; + +test('CLI', async (t) => { + const messages = []; + const { log: origLog, error: origError } = console; + + const log = (...args) => { messages.push(['log', ...args]); origLog(...args); }; + const error = (...args) => { messages.push(['error', ...args]); origError(...args); }; + + const restoreLog = mockProperty(console, 'log', { value: log }); + t.teardown(restoreLog); + const restoreError = mockProperty(console, 'error', { value: error }); + t.teardown(restoreError); + + await require('../bin/scorecard-cli'); // eslint-disable-line global-require + + restoreLog(); + restoreError(); + + t.deepEqual( + messages, + [['log', messages[0][1]]], + 'expected console messages are output', + ); +});