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(action-attest-npmjs-com): init the action #52

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
21 changes: 21 additions & 0 deletions actions/attest-for-npmsjs-com/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024-present Ledger https://www.ledger.com/

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
49 changes: 49 additions & 0 deletions actions/attest-for-npmsjs-com/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# GitHub Action: `attest-for-npmsjs-com`

<!-- action-docs-description source="action.yml" -->

<!-- action-docs-description source="action.yml" -->

## Usage

### Permissions

To enable this action to work properly, ensure the following permissions are set in your workflow:

```yaml
permissions:
id-token: write
```

### Example Workflow

Here's how you can use the `attest` action within your workflow:

```yaml
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Attest for npmjs.com
id: attest
uses: LedgerHQ/actions-security/actions/attest-for-npmsjs-com@actions/attest-for-npmsjs-com-1
with:
subject-path: path/to/my/npm-package/to/attest
```

<!-- action-docs-inputs source="action.yml" -->
## Inputs

| name | description | required | default |
| --- | --- | --- | --- |
| `subject-path` | <p>Path to the npm package to attest.</p> | `true` | `""` |
<!-- action-docs-inputs source="action.yml" -->

<!-- action-docs-outputs source="action.yml" -->

<!-- action-docs-outputs source="action.yml" -->


## Runs

This action is a **composite action**, which allows us to combine multiple workflow steps into a single, reusable action. This promotes modularity and simplifies our workflows.
137 changes: 137 additions & 0 deletions actions/attest-for-npmsjs-com/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
name: "[Ledger Security] Attest a npm package for npmjs.com distribution"
description: ""
inputs:
subject-path:
description: 'Path to the npm package to attest.'
required: true
github_token:
description: 'GitHub Token (to be able to upload the attestation to the GitHub Attestation API).'
default: ${{ github.token }}
required: true

runs:
using: "composite"
steps:
- name: Check if repository is public (signature are leaking private information)
if: ${{ github.event.repository.visibility != 'public' }}
shell: bash
run: |
echo "::error This action only runs on public repositories. To avoid leaking private information, the action will be stopped."
exit 1
- name: Set GitHub Path
run: echo "$GITHUB_ACTION_PATH" >> $GITHUB_PATH
shell: bash
env:
GITHUB_ACTION_PATH: ${{ github.action_path }}
- name: Auto-Detect - Package details
id: package-details
shell: bash
run: |
if [ -f "${{ inputs.subject-path }}" ]; then
PACKAGE_PATH=${{ inputs.subject-path }}
else
PACKAGE_PATH=$(ls -1 ${{ inputs.subject-path }}/*.tgz | head -n 1)
fi

pack_json=$(npm pack $PACKAGE_PATH --json | tee pack.json | jq -c)
jq <pack.json

PACKAGE_FILENAME=$(echo "$pack_json" | jq -r '.[0].filename')
PACKAGE_NAME=$(echo "$pack_json" | jq -r '.[0].name')
PACKAGE_VERSION=$(echo "$pack_json" | jq -r '.[0].version')
PACKAGE_INTEGRITY=$(echo "$pack_json" | jq -r '.[0].integrity')
echo "PACKAGE_FILENAME=${PACKAGE_FILENAME}" >> $GITHUB_OUTPUT
echo "PACKAGE_NAME=${PACKAGE_NAME}" >> $GITHUB_OUTPUT
echo "PACKAGE_VERSION=${PACKAGE_VERSION}" >> $GITHUB_OUTPUT
echo "PACKAGE_INTEGRITY=${PACKAGE_INTEGRITY}" >> $GITHUB_OUTPUT
- name: Print all outputs of packges details
shell: bash
run: |
echo "PACKAGE_FILENAME=${{ steps.package-details.outputs.PACKAGE_FILENAME }}"
echo "PACKAGE_NAME=${{ steps.package-details.outputs.PACKAGE_NAME }}"
echo "PACKAGE_VERSION=${{ steps.package-details.outputs.PACKAGE_VERSION }}"
echo "PACKAGE_INTEGRITY=${{ steps.package-details.outputs.PACKAGE_INTEGRITY }}"
- name: Generate the Predicate
shell: bash
env:
BUILDER_ID: ${{ github.workflow_ref }} # Buid is made on the same job
CONFIG_SOURCE_URI: git+${{ github.event.repository.html_url }}@${{ github.ref }}
CONFIG_SOURCE_DIGEST: ${{ github.sha }}
GITHUB_ACTOR_ID: ${{ github.actor_id }}
GITHUB_TRIGGERING_ACTOR_ID: ${{ github.triggering_actor }}
GITHUB_REPOSITORY_ID: ${{ github.repository_id }}
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
GITHUB_REPOSITORY_OWNER_ID: ${{ github.repository_owner_id}}
GITHUB_RUN_ID: ${{ github.run_id }}
GITHUB_RUN_ATTEMPT: ${{ github.run_attempt }}
GITHUB_RUN_NUMBER: ${{ github.run_number }}
GITHUB_SHA: ${{ github.sha }}
GITHUB_REF: ${{ github.ref }}
GITHUB_REF_TYPE: ${{ github.ref_type }}
GITHUB_EVENT_NAME: ${{ github.event_name }}
GITHUB_WORKFLOW: ${{ github.workflow }}
GITHUB_BASE_REF: ${{ github.base_ref }}
GITHUB_WORKFLOW_REF: ${{ github.workflow_ref }}
BUILD_INVOCATION_ID: ${{ github.run_id }}-${{ github.run_attempt }}
run: slsa-github-generator-nodejs-predicate.sh
- name: Generate the SLSA layout
shell: bash
env:
SLSA_OUTPUTS_ARTIFACTS_FILE: artifacts-layout.json
PACKAGE_NAME: ${{ steps.package-details.outputs.PACKAGE_NAME }}
PACKAGE_VERSION: ${{ steps.package-details.outputs.PACKAGE_VERSION }}
PACKAGE_INTEGRITY: ${{ steps.package-details.outputs.PACKAGE_INTEGRITY }}
PACKAGE_FILENAME: ${{ steps.package-details.outputs.PACKAGE_FILENAME }}
run: slsa-github-generator-nodejs-layout.sh
- name: Generate the attestation
uses: slsa-framework/slsa-github-generator/.github/actions/[email protected]
with:
slsa-layout-file: artifacts-layout.json
predicate-type: https://slsa.dev/provenance/v0.2
predicate-file: predicate.json
output-folder: attestations
- name: Sign the attestation
uses: slsa-framework/slsa-github-generator/.github/actions/[email protected]
with:
payload-type: application/vnd.in-toto+json
attestations: attestations
output-folder: attestations-signed
- name: Scan to find the attestation signed from the signed folder
shell: bash
id: scan-attestations-signed
run: |
ATTESTATION_SIGNED_PATH=$(ls -1 ./attestations-signed/*.build.slsa | head -n 1)
if [ -z "$ATTESTATION_SIGNED_PATH" ]; then
echo "Error: No attestation signed files found in the ./attestations-signed directory."
exit 1
fi
echo "ATTESTATION_SIGNED_PATH=${ATTESTATION_SIGNED_PATH}" >> $GITHUB_OUTPUT
- name: Upload Attestation to the Github API
uses: actions/github-script@v6
env:
ATTESTATION_PATH: ${{ steps.scan-attestations-signed.outputs.ATTESTATION_SIGNED_PATH }}
GITHUB_REPOSITORY: ${{ github.repository }}
with:
script: |
const fs = require('fs');
const attestationPath = process.env.ATTESTATION_PATH;
const attestationContent = fs.readFileSync(attestationPath, 'utf8');
const attestation = JSON.parse(attestationContent);
const [owner, repoName] = process.env.GITHUB_REPOSITORY.split('/');

const response = await github.request('POST /repos/{owner}/{repo}/attestations', {
owner: owner,
repo: repoName,
headers: {
'X-GitHub-Api-Version': '2022-11-28'
},
data: {
bundle: attestation
}
});

if (response.status !== 201) {
throw new Error(`Failed to upload attestation to GitHub API. HTTP status code: ${response.status}`);
}

console.log('Attestation uploaded successfully:', response.data);
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/bin/bash -eu
#
# Copyright 2023 SLSA Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

set -euo pipefail

# We will encode the subject name as an npm package url (purl).
# https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst
#
# The npm package's scope is considered a purl "namespace" and not part of the
# package name. So the subject will take the form of:
#
# With scope:
# pkg:npm/<scope>/<name>@<version>
#
# Without scope:
# pkg:npm/<name>@<version>
#
# Each of scope, name, and version are URL(percent) encoded.

# Get the raw package name and scope from the output of `npm pack --json`
# This name is of the form '<scope>/<package name>'
# NOTE: `cut -s` will suppress output for all fields if the delimiter is
# not present.
raw_package_scope=$(echo "${PACKAGE_NAME:-}" | cut -s -d'/' -f1)
raw_package_name=$(echo "${PACKAGE_NAME:-}" | cut -s -d'/' -f2)
if [ "${raw_package_name}" == "" ]; then
# This is a non-scoped package.
raw_package_name="${PACKAGE_NAME:-}"
raw_package_scope=""
fi
# package scope (namespace) is URL(percent) encoded.
package_scope=$(echo "\"${raw_package_scope}\"" | jq -r '. | @uri')
# package name is URL(percent) encoded.
package_name=$(echo "\"${raw_package_name}\"" | jq -r '. | @uri')
# version is URL(percent) encoded. This is the version from the project's
# package.json and could be a commit, or any string by the user. It does not
# actually have to be a version number and is not validated as such by npm.
package_version=$(echo "\"${PACKAGE_VERSION:-}\"" | jq -r '. | @uri')

package_id="${package_name}@${package_version}"
if [ "${package_scope}" != "" ]; then
package_id="${package_scope}/${package_id}"
fi
subject_name="pkg:npm/${package_id}"

# The integrity digest is formatted as follows:
#
# <hash alg>-<base64 encoded checksum>
#
# For example:
# sha512-geEornsf879/Ygi9byQq/mpYboMcIKiGUxJ+RgHM3DCxqnOx15ttF5FparP/ZSITHTLM39MWVhW9qPa4XxtuSg==
integrity_digest="${PACKAGE_INTEGRITY:-}"

# We will parse out the checksum hash algorithm used.
# NOTE: ensure lowercase just to make sure.
alg=$(echo "${integrity_digest}" | cut -d'-' -f1 | tr '[:upper:]' '[:lower:]')
# Here we parse out the checksum and convert it from base64 to hex. 'od' seems
# to be the standard tool to do this kind conversion on Linux.
digest=$(echo "${integrity_digest}" | cut -d'-' -f2- | base64 -d | od -A n -v -t x1 | tr -d ' \n')

# NOTE: the name of the attestation should be configurable.
filename=${PACKAGE_FILENAME:-}
attestation_name="${filename%.*}"
cat <<EOF | jq | tee "${SLSA_OUTPUTS_ARTIFACTS_FILE}"
{
"version": 1,
"attestations":
[
{
"name": "${attestation_name}",
"subjects":
[
{
"name": "${subject_name}",
"digest":
{
"${alg}": "${digest}"
}
}
]
}
]
}
EOF

echo "attestation-name=${attestation_name}.build.slsa" >>"${GITHUB_OUTPUT}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/bin/bash

# Compute some variables
extract_entry_point() {
local workflow_ref="$1"
local entry_point="${workflow_ref#*/*/}"
entry_point="${entry_point%@*}"
echo "$entry_point"
}

ENTRY_POINT=$(extract_entry_point "$GITHUB_WORKFLOW_REF")

# Generate JSON
cat <<EOF > predicate.json
{
"builder": {
"id": "$BUILDER_ID"
},
"buildType": "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
"invocation": {
"configSource": {
"uri": "$CONFIG_SOURCE_URI",
"digest": {
"sha1": "$CONFIG_SOURCE_DIGEST"
},
"entryPoint": "$ENTRY_POINT"
},
"environment": {
"GITHUB_ACTOR_ID": "$GITHUB_ACTOR_ID",
"GITHUB_EVENT_NAME": "$GITHUB_EVENT_NAME",
"GITHUB_BASE_REF": "$GITHUB_BASE_REF",
"GITHUB_REF": "$GITHUB_REF",
"GITHUB_REF_TYPE": "$GITHUB_REF_TYPE",
"GITHUB_REPOSITORY": "$GITHUB_REPOSITORY",
"GITHUB_REPOSITORY_ID": "$GITHUB_REPOSITORY_ID",
"GITHUB_REPOSITORY_OWNER": "$GITHUB_REPOSITORY_OWNER",
"GITHUB_REPOSITORY_OWNER_ID": "$GITHUB_REPOSITORY_OWNER_ID",
"GITHUB_RUN_ATTEMPT": "$GITHUB_RUN_ATTEMPT",
"GITHUB_RUN_ID": "$GITHUB_RUN_ID",
"GITHUB_RUN_NUMBER": "$GITHUB_RUN_NUMBER",
"GITHUB_SHA": "$GITHUB_SHA",
"GITHUB_TRIGGERING_ACTOR_ID": "$GITHUB_TRIGGERING_ACTOR_ID",
"GITHUB_WORKFLOW_REF": "$GITHUB_WORKFLOW_REF",
"GITHUB_WORKFLOW_SHA": "$GITHUB_WORKFLOW_SHA"
}
},
"metadata": {
"buildInvocationId": "$BUILD_INVOCATION_ID"
},
"materials": [
{
"uri": "$CONFIG_SOURCE_URI",
"digest": {
"sha1": "$CONFIG_SOURCE_DIGEST"
}
}
]
}
EOF

echo "Predicate file generated: predicate.json"