diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b0239f6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + # Wait 7 days after a release is published before opening an update PR, + # so freshly published (potentially compromised) versions are not pinned + # immediately. Keeps the SHA pins refreshed as reviewable PRs. + cooldown: + default-days: 7 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..ca321df --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,28 @@ +name: Lint + +# Static checks for the repo's shell scripts. Runs the same tests/run.sh that +# developers run locally, so local and CI can never check different things. +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + shell-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Ensure shellcheck is available + run: | + if ! command -v shellcheck >/dev/null 2>&1; then + sudo apt-get update && sudo apt-get install -y shellcheck + fi + shellcheck --version + + - name: Run lint entrypoint + run: ./tests/run.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8fdc7b --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Python bytecode / caches (sarif/ converters) +__pycache__/ +*.py[cod] + +# OS metadata +.DS_Store +Thumbs.db + +# Editor / IDE +.idea/ +.vscode/ +*.swp + +# Local checkout the demo scripts clone into, plus any scan artifacts +# (openapi-spec.yml, scan-results.txt, results.sarif) written inside it. +/java-github-actions-demo/ + +# Agent sandbox state: local tooling, never part of the repo. +.agent-sandbox-config/ + +# Local analysis / work products, kept out of version control by convention. +analysis/ diff --git a/demo-scripts/github-actions-demo.sh b/demo-scripts/github-actions-demo.sh index 35b77bb..713b691 100755 --- a/demo-scripts/github-actions-demo.sh +++ b/demo-scripts/github-actions-demo.sh @@ -1,42 +1,88 @@ -#!/bin/bash -set -e +#!/usr/bin/env bash +set -euo pipefail -# Clone the repository locally from your terminal -git clone https://github.com/$(git config user.name)/java-github-actions-demo.git -cd java-github-actions-demo - -# add the nightvision token as the github action secret NIGHTVISION_TOKEN -nightvision login -nightvision token create > token +# --------------------------------------------------------------------------------------------------------------------- +# NightVision GitHub Actions demo +# +# Clones your fork of java-github-actions-demo, wires the NIGHTVISION_TOKEN Actions secret, +# creates a NightVision app + target, records auth, and pushes a commit to trigger the +# workflow. +# +# Re-run behaviour: this script is intended to be safe to re-run. The create steps +# (NightVision app/target) are guarded so a second run reuses existing resources with a +# logged warning instead of aborting. The NightVision token is the exception: each run +# creates a fresh one and overwrites the Actions secret (the value cannot be read back, so +# it is rotated, not reused); older tokens stay in NightVision, so revoke them there if you +# re-run often. The script does NOT delete anything; see the cleanup notes at the bottom. +# --------------------------------------------------------------------------------------------------------------------- -# Check if GitHub CLI is authenticated +# Authenticate to GitHub and NightVision first, so the steps below only run after both +# logins are confirmed - an aborted login then leaves nothing half-created. if ! gh auth status 2>&1 | grep -q 'Logged in to github.com'; then echo "GitHub CLI not authenticated. Running 'gh auth login'..." gh auth login else echo "Logged in to github already..." fi +nightvision login + +# Clone your fork of the demo repository. The owner comes from the authenticated GitHub +# login: 'git config user.name' is a display name, not a valid URL segment for most users. +# Idempotent: reuse an existing checkout instead of failing on a second run, but only +# after confirming it points at the expected fork - a stale checkout of another repo +# would otherwise receive the push at the end of the script. +OWNER="$(gh api user --jq .login)" +if [ ! -d java-github-actions-demo ]; then + git clone "https://github.com/$OWNER/java-github-actions-demo.git" +elif ! git -C java-github-actions-demo remote get-url origin | grep -qiE "github\.com[:/]$OWNER/java-github-actions-demo(\.git)?$"; then + echo "ERROR: existing ./java-github-actions-demo does not point at $OWNER/java-github-actions-demo; move it aside and re-run." >&2 + exit 1 +fi +cd java-github-actions-demo + +# Create a fresh NightVision token for the NIGHTVISION_TOKEN Actions secret, without +# writing it to disk. tr strips any stray whitespace; the guard catches an empty result +# (an exit-0 token create with no output) before it becomes an empty secret. +TOKEN="$(nightvision token create | tr -d '[:space:]')" +if [ -z "$TOKEN" ]; then + echo "ERROR: 'nightvision token create' returned an empty token; aborting." >&2 + exit 1 +fi -# Use GitHub CLI to set NIGHTVISION_TOKEN -gh secret set NIGHTVISION_TOKEN < token -rm token +# Set the Actions secret to the freshly created token (rotated every run, not reused, +# because a token's value cannot be read back). 'gh secret set' upserts, so no +# already-exists guard is needed; a real failure aborts the script (set -e). The explicit +# --repo pins the secret to your fork rather than inferring it from the checkout's remote. +gh secret set NIGHTVISION_TOKEN --body "$TOKEN" --repo "$OWNER/java-github-actions-demo" -# Create app and target -# Username: user -# Password: password +# --------------------------------------------------------------------------------------------------------------------- +# NightVision commands +# --------------------------------------------------------------------------------------------------------------------- +# Create app and target. Guarded so a re-run reuses the existing app/target. URL="https://localhost:9000" APP="javaspringvulny-api" -nightvision app create $APP -nightvision target create $APP $URL --type API +nightvision app create "$APP" || echo "WARNING: 'nightvision app create $APP' failed (it may already exist); continuing." +nightvision target create "$APP" "$URL" --type API || echo "WARNING: 'nightvision target create $APP' failed (it may already exist); continuing." + # Start the application docker compose up -d; sleep 10 -# Record authentication - click on Form Auth -echo "Click on Form Auth and use these credentials: " -echo "\tUsername: user" -echo "\tPassword: password" -nightvision auth playwright create $APP $URL - -# Add a commit and trigger the CI/CD -echo "foobar" >> README.md -git commit -am 'trigger a github action' +# Record authentication - click on Form Auth. +# These are the demo application's default credentials (the javaspringvulny sample app), +# not real secrets. +echo "Click on Form Auth and use the javaspringvulny demo defaults:" +echo " Username: user" +echo " Password: password" +nightvision auth playwright create "$APP" "$URL" + +# --------------------------------------------------------------------------------------------------------------------- +# Add an empty commit and push to trigger the GitHub Actions workflow. An empty +# commit always provides a fresh commit to push (including on a re-run against an +# existing checkout) without modifying tracked files. +# --------------------------------------------------------------------------------------------------------------------- +git commit --allow-empty -m "Trigger GitHub Actions workflow" git push + +# Notes: +# To clean up (substitute your GitHub login): +# gh secret delete NIGHTVISION_TOKEN --repo /java-github-actions-demo +# rm -rf ./java-github-actions-demo diff --git a/demo-scripts/gitlab-demo.sh b/demo-scripts/gitlab-demo.sh index ee7c191..ff94153 100755 --- a/demo-scripts/gitlab-demo.sh +++ b/demo-scripts/gitlab-demo.sh @@ -1,5 +1,21 @@ #!/usr/bin/env bash -set -e +set -euo pipefail + +# --------------------------------------------------------------------------------------------------------------------- +# NightVision GitLab "Easy Mode" demo (NV-4418) +# +# Creates a GitLab project, wires the NIGHTVISION_TOKEN CI variable, creates a NightVision +# app + target, records auth, and pushes the demo repo to trigger the CI/CD pipeline. +# +# Re-run behaviour: this script is intended to be safe to re-run. The create steps +# (GitLab repo, NightVision app/target) are guarded so a second run reuses existing +# resources with a logged warning instead of aborting, and the gitlab remote is +# reconciled to the current "$GROUP/$REPO" on each run. The NightVision token is +# the exception: each run creates a fresh one and overwrites the CI variable (the value +# cannot be read back, so it is rotated, not reused); older tokens stay in NightVision, so +# revoke them there if you re-run often. The script does NOT delete anything; see the +# cleanup notes at the bottom to tear a demo down. +# --------------------------------------------------------------------------------------------------------------------- # Check if the correct number of arguments is provided if [ "$#" -ne 2 ]; then @@ -9,70 +25,108 @@ if [ "$#" -ne 2 ]; then fi # Assign positional arguments to variables -GROUP=$1 -REPO=$2 +GROUP="$1" +REPO="$2" echo "Creating a repository under: $GROUP/$REPO" -# Clone the GitHub repository that we will mirror to GitLab -git clone https://github.com/nvsecurity/java-github-actions-demo +# Clone the GitHub repository that we will mirror to GitLab. +# Idempotent: reuse an existing checkout instead of failing on a second run. +if [ ! -d java-github-actions-demo ]; then + git clone https://github.com/nvsecurity/java-github-actions-demo +fi cd java-github-actions-demo # --------------------------------------------------------------------------------------------------------------------- # Set up the GitLab repository # --------------------------------------------------------------------------------------------------------------------- -# Create a repository -echo "NOTE: Select NO for 'Create a local project directory'" -glab repo create $REPO - -# add the nightvision token as the GitLab secret NIGHTVISION_TOKEN -nightvision login -TOKEN=$(nightvision token create) - -# Check if GitLab CLI is authenticated +# Authenticate to GitLab and NightVision first, so the create steps below only run after +# both logins are confirmed - an aborted login then leaves nothing half-created. if ! glab auth status 2>&1 | grep -q 'Logged in to gitlab.com'; then echo "GitLab CLI not authenticated. Running 'glab auth login'..." glab auth login else echo "Logged in to gitlab already..." fi +nightvision login -glab variable set NIGHTVISION_TOKEN --masked --repo $GROUP/$REPO $TOKEN < token +# Create a repository in the target namespace. If it already exists, keep going. +echo "NOTE: Select NO for 'Create a local project directory'" +glab repo create "$GROUP/$REPO" || echo "WARNING: 'glab repo create $GROUP/$REPO' failed (the project may already exist); continuing." + +# Create a fresh NightVision token for the NIGHTVISION_TOKEN CI variable. tr strips any +# stray whitespace so the value GitLab masks is clean; the guard catches an empty result +# (an exit-0 token create with no output) before it becomes an invalid masked variable. +TOKEN="$(nightvision token create | tr -d '[:space:]')" +if [ -z "$TOKEN" ]; then + echo "ERROR: 'nightvision token create' returned an empty token; aborting." >&2 + exit 1 +fi + +# Set the masked CI variable to the freshly created token (rotated every run, not reused, +# because a token's value cannot be read back). If it already exists (older glab errors +# instead of upserting), update it. This must succeed: failing both set and update aborts +# the script (set -e) rather than silently leaving CI a stale token. +glab variable set NIGHTVISION_TOKEN --masked --repo "$GROUP/$REPO" "$TOKEN" \ + || glab variable update NIGHTVISION_TOKEN --masked --repo "$GROUP/$REPO" "$TOKEN" # --------------------------------------------------------------------------------------------------------------------- # Note that GitLab has additional requirements vs other CI/CD providers. # Instead of `localhost` you must use the `docker` hostname. -# First add the docker hostname reference to your `/etc/hosts` file on your laptop +# First add the docker hostname reference to your `/etc/hosts` file on your laptop. +# +# This edits a system file with sudo. It is left in place after the demo so repeat runs work. +# To revert it afterwards run: +# sudo sed -i.bak '/^127\.0\.0\.1 docker$/d' /etc/hosts # --------------------------------------------------------------------------------------------------------------------- -if ! grep -q "127.0.0.1 docker" /etc/hosts; then \ - echo "127.0.0.1 docker" | sudo tee -a /etc/hosts; \ +if ! grep -q "127.0.0.1 docker" /etc/hosts; then + echo "NOTICE: adding '127.0.0.1 docker' to /etc/hosts (sudo). See the revert command in this script's comments." + echo "127.0.0.1 docker" | sudo tee -a /etc/hosts fi + # --------------------------------------------------------------------------------------------------------------------- # NightVision commands # --------------------------------------------------------------------------------------------------------------------- -# Create app and target -# Username: user -# Password: password +# Create app and target. Guarded so a re-run reuses the existing app/target. URL="https://docker:9000" APP="javaspringvulny-api-gitlab" -nightvision app create $APP -nightvision target create $APP https://docker:9000 --type api +nightvision app create "$APP" || echo "WARNING: 'nightvision app create $APP' failed (it may already exist); continuing." +nightvision target create "$APP" "$URL" --type API || echo "WARNING: 'nightvision target create $APP' failed (it may already exist); continuing." # Start the application docker compose up -d; sleep 10 -# Record authentication - click on Form Auth -echo "Click on Form Auth and use these credentials: " -echo "\tUsername: user" -echo "\tPassword: password" -nightvision auth playwright create $APP $URL +# Record authentication - click on Form Auth. +# These are the demo application's default credentials (the javaspringvulny sample app), +# not real secrets. +echo "Click on Form Auth and use the javaspringvulny demo defaults:" +echo " Username: user" +echo " Password: password" +nightvision auth playwright create "$APP" "$URL" # --------------------------------------------------------------------------------------------------------------------- # sync it back with GitLab and trigger the CI/CD job. # --------------------------------------------------------------------------------------------------------------------- -git remote add gitlab git@gitlab.com:$GROUP/$REPO.git +# Point the gitlab remote at the requested project. A reused checkout may carry the +# remote from a previous run with different arguments; reconciling it to the current +# "$GROUP/$REPO" keeps the push from silently targeting the previous run's project. +if git remote get-url gitlab >/dev/null 2>&1; then + git remote set-url gitlab "git@gitlab.com:$GROUP/$REPO.git" +else + git remote add gitlab "git@gitlab.com:$GROUP/$REPO.git" +fi +# Add an empty commit so each run pushes a fresh commit and triggers the +# pipeline. A re-run reuses the existing checkout, which has no new commits, so a +# bare push would be "Everything up-to-date" and fire nothing. +git commit --allow-empty -m "Trigger GitLab pipeline" git push gitlab main # Notes: -# To delete the project: -# glab repo delete $GROUP/$REPO +# To delete the project and local checkout (substitute the group and repo you ran this script with): +# glab repo delete / # rm -rf ./java-github-actions-demo +# The NightVision app and target are not deleted; they are reused on re-run +# (auth is re-recorded each run, and each run mints a fresh token that persists, +# as noted in the header). Remove the app, target, and stale tokens from the +# NightVision UI for a full teardown. +# To revert the /etc/hosts entry: +# sudo sed -i.bak '/^127\.0\.0\.1 docker$/d' /etc/hosts diff --git a/tests/run.sh b/tests/run.sh new file mode 100755 index 0000000..235a592 --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Shared lint entrypoint for nv-public-reference. CI (.github/workflows/lint.yml) +# invokes this exact script, so a local run and a CI run can never check different +# things or drift apart over time. +# +# What it checks, for every tracked shell script: +# 1. bash -n - syntax / parse check +# 2. shellcheck - static analysis (quoting, set -e pitfalls, unsafe expansions) +# +# Usage: tests/run.sh +set -euo pipefail + +# Run from the repository root regardless of the caller's working directory, so the +# git file list and relative paths resolve identically in local and CI runs. +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$repo_root" + +# The shellcheck tool is required, not optional: silently skipping it when absent +# would let a local run pass on a weaker check than CI, which is the exact drift +# this shared entrypoint exists to prevent. +if ! command -v shellcheck >/dev/null 2>&1; then + echo "ERROR: shellcheck not found on PATH. Install it and re-run:" >&2 + echo " macOS: brew install shellcheck" >&2 + echo " Debian: sudo apt-get install -y shellcheck" >&2 + echo " other: https://github.com/koalaman/shellcheck#installing" >&2 + exit 1 +fi + +# Enumerate tracked shell scripts via git so untracked / vendored files (for example +# the .agent-sandbox-config tree) are never linted. A read loop (rather than mapfile) +# keeps this working on bash 3.2, the default on macOS, so local runs match CI. +scripts=() +while IFS= read -r script_path; do + scripts+=("$script_path") +done < <(git ls-files '*.sh') +if [ "${#scripts[@]}" -eq 0 ]; then + echo "No tracked *.sh files found; nothing to lint." + exit 0 +fi + +echo "Linting ${#scripts[@]} shell script(s):" +printf ' %s\n' "${scripts[@]}" + +# Cheap parse check first; set -e aborts on the first failure so CI fails the job. +for script in "${scripts[@]}"; do + bash -n "$script" +done + +# Then the deeper static analysis pass over the whole set in one invocation. +shellcheck "${scripts[@]}" + +echo "OK: all shell scripts passed bash -n and shellcheck."