From 0acad6aef6915ee589e37fbfbc291bbf15a91d7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Aveiro?= Date: Wed, 7 Feb 2024 12:06:26 +0000 Subject: [PATCH] feat: first release --- .github/configs/commitlint.config.js | 27 +++++ .github/workflows/checks.base.yaml | 16 +++ .github/workflows/checks.branches.yaml | 11 ++ .github/workflows/release.yaml | 46 ++++++++ .gitignore | 103 +++++++++++++++++ .releaserc.yaml | 129 +++++++++++++++++++++ LICENSE | 21 ++++ README.md | 152 +++++++++++++++++++++++++ action.yaml | 96 ++++++++++++++++ assets/logo.webp | Bin 0 -> 4754 bytes 10 files changed, 601 insertions(+) create mode 100644 .github/configs/commitlint.config.js create mode 100644 .github/workflows/checks.base.yaml create mode 100644 .github/workflows/checks.branches.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 .gitignore create mode 100644 .releaserc.yaml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 action.yaml create mode 100644 assets/logo.webp diff --git a/.github/configs/commitlint.config.js b/.github/configs/commitlint.config.js new file mode 100644 index 0000000..2c3951d --- /dev/null +++ b/.github/configs/commitlint.config.js @@ -0,0 +1,27 @@ +const { maxLineLength } = require('@commitlint/ensure') + +const bodyMaxLineLength = 100 + +const validateBodyMaxLengthIgnoringDeps = (parsedCommit) => { + const { type, scope, body } = parsedCommit + const isDepsCommit = + type === 'chore' && scope === 'release' + + return [ + isDepsCommit || !body || maxLineLength(body, bodyMaxLineLength), + `body's lines must not be longer than ${bodyMaxLineLength}`, + ] +} + +module.exports = { + extends: ['@commitlint/config-conventional'], + plugins: ['commitlint-plugin-function-rules'], + rules: { + 'body-max-line-length': [0], + 'function-rules/body-max-line-length': [ + 2, + 'always', + validateBodyMaxLengthIgnoringDeps, + ], + }, +} diff --git a/.github/workflows/checks.base.yaml b/.github/workflows/checks.base.yaml new file mode 100644 index 0000000..015fc18 --- /dev/null +++ b/.github/workflows/checks.base.yaml @@ -0,0 +1,16 @@ +name: Checks (base) + +on: + workflow_call: + +jobs: + commitlint: + name: (check) Commitlint + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - name: Run Commitlint + uses: wagoid/commitlint-github-action@v5 + with: + configFile: .github/configs/commitlint.config.js diff --git a/.github/workflows/checks.branches.yaml b/.github/workflows/checks.branches.yaml new file mode 100644 index 0000000..f22bfbf --- /dev/null +++ b/.github/workflows/checks.branches.yaml @@ -0,0 +1,11 @@ +name: Checks (branches) + +on: + push: + branches-ignore: + - main + - staging + +jobs: + checks: + uses: ./.github/workflows/checks.base.yaml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..00961b4 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,46 @@ +name: Release +run-name: "Release `${{ github.ref_name }}` (SHA: ${{ github.sha }})" + +on: + push: + branches: + - main + - staging + +permissions: + contents: read + +jobs: + checks: + name: Run Checks + uses: ./.github/workflows/checks.base.yaml + release: + name: Release + needs: checks + runs-on: ubuntu-latest + permissions: + contents: write + issues: write + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "lts/*" + - name: Install plugins + run: > + npm install -D + @semantic-release/git + @semantic-release/changelog + conventional-changelog-conventionalcommits + @saithodev/semantic-release-backmerge + - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies + run: npm audit signatures + - name: Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: npx semantic-release diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..47fb503 --- /dev/null +++ b/.gitignore @@ -0,0 +1,103 @@ +# Dependency directory +node_modules + +# Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# OS metadata +.DS_Store +Thumbs.db + +# Ignore built ts files +__tests__/runner/* + +# IDE files +.idea +.vscode +*.code-workspace diff --git a/.releaserc.yaml b/.releaserc.yaml new file mode 100644 index 0000000..cb54f1d --- /dev/null +++ b/.releaserc.yaml @@ -0,0 +1,129 @@ +preset: conventionalcommits +tagFormat: "${version}" + +branches: + - "+([0-9])?(.{+([0-9]),x}).x" + - main + - next + - next-major + - name: staging + prerelease: rc + - name: beta + prerelease: true + - name: alpha + prerelease: true + +plugins: + - "@semantic-release/commit-analyzer" + - "@semantic-release/release-notes-generator" + - "@semantic-release/changelog" + - "@semantic-release/git" + - "@semantic-release/github" + - "@saithodev/semantic-release-backmerge" + +verifyConditions: + - "@semantic-release/changelog" + - "@semantic-release/git" + - "@semantic-release/github" + - path: "@saithodev/semantic-release-backmerge" + backmergeBranches: + - from: main + to: staging + +analyzeCommits: + - path: "@semantic-release/commit-analyzer" + releaseRules: + - breaking: true + release: major + - type: build + release: patch + - type: chore + release: false + - type: ci + release: false + - type: docs + release: patch + - type: feat + release: minor + - type: fix + release: patch + - type: perf + release: patch + - type: refactor + release: false + - type: revert + release: patch + - type: style + release: false + - type: test + release: false + +generateNotes: + - path: "@semantic-release/release-notes-generator" + writerOpts: + groupBy: type + commitGroupsSort: title + commitsSort: header + linkCompare: true + linkReferences: true + presetConfig: + types: + - type: build + section: "๐ŸฆŠ CI/CD" + hidden: false + - type: chore + section: "Other" + hidden: true + - type: ci + section: "๐ŸฆŠ CI/CD" + hidden: false + - type: docs + section: "๐Ÿ“” Docs" + hidden: false + - type: example + section: "๐Ÿ“ Examples" + hidden: true + - type: feat + section: "๐Ÿš€ Features" + hidden: false + - type: fix + section: "๐Ÿ›  Fixes" + hidden: false + - type: perf + section: "โฉ Performance" + hidden: false + - type: refactor + section: ":scissors: Refactor" + hidden: false + - type: revert + section: "๐Ÿ™…โ€โ™‚๏ธ Reverts" + hidden: false + - type: style + section: "๐Ÿ’ˆ Style" + hidden: false + - type: test + section: "๐Ÿงช Tests" + hidden: false + +prepare: + - path: "@semantic-release/changelog" + changelogFile: CHANGELOG.md + + - path: "@semantic-release/git" + message: "chore(release): release <%= nextRelease.version %> - <%= new Date().toLocaleDateString('en-US', {year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' }) %> \n\n<%= nextRelease.notes %>" + assets: + - CHANGELOG.md + - pyproject.toml + +publish: + - path: "@semantic-release/github" + +success: + - path: "@semantic-release/github" + - path: "@saithodev/semantic-release-backmerge" + backmergeBranches: + - from: main + to: staging + +fail: + - path: "@semantic-release/github" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3cd08a8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Ethiack, Lda. + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6398e44 --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ + + +[version-shield]: https://img.shields.io/github/v/release/ethiack/github-action?style=for-the-badge +[version-url]: https://github.com/ethiack/github-action/releases/latest + +[license-shield]: https://img.shields.io/github/license/ethiack/github-action?style=for-the-badge +[license-url]: LICENSE + +[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 +[linkedin-url]: https://linkedin.com/company/ethiack + + + + +
+ +

+
+ logo +

+ Ethiack GitHub Action +

+

+ +

GitHub Action for integrating Ethiack's Public API in GitHub Workflows.

+ +[![GitHub Release][version-shield]][version-url] +[![MIT License][license-shield]][license-url] +[![LinkedIn][linkedin-shield]][linkedin-url] + +
+ + +[Introduction](#introduction) โ€ข +[Credentials Setup](#credentials-setup) โ€ข +[Usage](#usage) โ€ข +[License](#license) + + +
+ +## Introduction + +This GitHub Action facilitates the integration of [Ethiack's Public API](https://api.ethiack.com) ([API docs](https://portal.ethiack.com/docs/api/)) for launching scans through GitHub workflows. By utilizing this action, you can seamlessly incorporate Ethiack's security scanning capabilities into your CI/CD pipeline, enhancing your software development lifecycle with automated security testing. + +

(back to top)

+ +## Credentials Setup + + +Using Ethiack's API - and, therefore, this Action - requires authentication using an *API Key* and *API Secret*, which can be retrieved in [Ethiack's Portal settings page](https://portal.ethiack.com/settings/api). These credentials must be available as environment variables `ETHIACK_API_KEY` and `ETHIACK_API_SECRET`, repectively, whenever this action is used. + +To configure, navigate to your repository settings, select `Secrets and variables`, then `Actions`, and add the following secrets: + +- `ETHIACK_API_KEY`: Your API Key +- `ETHIACK_API_SECRET`: Your API Secret + +> [!CAUTION] +> Ensure that these secrets are referenced in your workflow files securely. + + +

(back to top)

+ +## Usage + + +> [!NOTE] +> This GitHub action is fundamentally a wrapper around [Ethiack's Public API](https://api.ethiack.com/), using [Ethiack's Job Manager Package](https://github.com/ethiack/job-manager). For more information, see the [API docs](https://portal.ethiack.com/docs/api/) and refer to the later package. + + +### **Example:** *Launching a job and waiting for its conclusion* + +This pipeline launches a scan for the domain `https://example.ethiack.com` and waits until it finishes (cf. `--wait` flag). If vulnerabilities with severity `medium` or higher are found, the success of the job is interpreted as failing, and this pipeline step will exit with a non-zero status code (cf. `--fail` flag). + +```yaml +jobs: + ethiack-scan: + runs-on: ubuntu-latest + steps: + - name: Launch Ethiack Scan + uses: ethiack/github-action@main + with: + command: launch + url: https://example.ethiack.com + args: --wait --fail --severity medium + env: + ETHIACK_API_SECRET: ${{ secrets.ETHIACK_API_SECRET }} + ETHIACK_API_KEY: ${{ secrets.ETHIACK_API_KEY }} +``` + + +### **Example:** *Checking the success of a job.* + +This pipeline checks the success of a job. It will fail if the respective job has finished and vulnerabilities with severity equal or above `high` were found. + + +```yaml +jobs: + ethiack-check-job-success: + runs-on: ubuntu-latest + steps: + - name: Check Ethiack Job Success + uses: ethiack/github-action@main + with: + command: success + uuid: 'your-job-uuid' + args: --severity high --fail + env: + ETHIACK_API_SECRET: ${{ secrets.ETHIACK_API_SECRET }} + ETHIACK_API_KEY: ${{ secrets.ETHIACK_API_KEY }} +``` + +> [!NOTE] +> For retrieving the success of a job without exiting the pipeline, simply provide the flag `--no-fail` instead of `--fail` in the previous example. + + +### Available commands + +This GitHub Action supports every command provided by [Ethiack's Job Manager Package](https://github.com/ethiack/job-manager). This includes, but is not necessarily limited to, the commands: + +
+ +| Command | Description | Required Inputs | +|:---------: |:----------------------------------------------------: |:---------------: | +| `check` | Check if a URL is valid and a job can be submitted | `url` | +| `launch` | Launch a job and, optionally, wait for it to finish. | `url` | +| `info` | Retrieve information about a job. | `uuid` | +| `list` | List all jobs for the organization. | - | +| `status` | Retrieve the status of a job. | `uuid` | +| `success` | Retrieve the success of a job. | `uuid` | +| `await` | Wait for a job to finish. | `uuid` | +| `cancel` | Cancel a queued or running job. | `uuid` | + +
+ + +#### Required Inputs + +> The `url` input refers to the target Uniform Resource Locator (URL) of the service for which the command is run. + +> The `uuid` input refers to the Universal Unique Identifier (UUID) of the job for which the command is run. + +#### Optional Arguments +> The behaviour of these commands can be customized with flags and additional parameters provided inn the `args:` variable in the workflow step (e.g., the `--fail` and `--severity` flags in the examples above). For more information regarding the available options and flags for each command, please refer to the [Job Manager Package](https://github.com/ethiack/job-manager). + +

(back to top)

+ + +## License +Distributed under the MIT License. See [LICENSE](LICENSE) for more information. + +

(back to top)

diff --git a/action.yaml b/action.yaml new file mode 100644 index 0000000..00d6389 --- /dev/null +++ b/action.yaml @@ -0,0 +1,96 @@ +name: 'Ethiack Job Manager Action' +description: 'Integrates Ethiack Job Manager with GitHub Actions for managing Artiacker jobs.' +author: 'Ethiack, Lda.' + +branding: + icon: 'search' + color: 'green' + +inputs: + command: + description: 'The command to run. Available options are "launch", "cancel", "info", "list", "status", "success", "await", and "check".' + required: true + uuid: + description: 'The UUID of the job. Required for commands "cancel", "info", "status", "success", and "await".' + required: false + default: '' + url: + description: 'The URL of the target service. Required for commands "launch", and "check".' + required: false + default: '' + args: + description: 'The arguments to pass to the job.' + required: false + default: '' + +outputs: + response: + description: 'The response from the Ethiack Job Manager package.' + value: ${{ steps.ethiack-run-command.outputs.response }} + +runs: + using: 'composite' + steps: + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Setup environment + run: | + python3 -m pip install --upgrade pip + python3 -m pip install --user --upgrade pipx + echo "PATH=$PATH:/root/.local/bin" >> $GITHUB_ENV + pipx install ethiack-job-manager + shell: bash + + - name: Execute Ethiack Job Manager Command + id: ethiack-run-command + run: | + # Run ethiack-job-manager + + set +e # Disable exit on error + + # Prepare command + COMMAND="ethiack-job-manager ${{ inputs.command }}" + + ## Add args + if [ -n "${{ inputs.args }}" ]; then + COMMAND="$COMMAND ${{ inputs.args }}" + fi + + ## Add url input if needed + if [ "${{ inputs.command }}" = "launch" ] || [ "${{ inputs.command }}" = "check" ]; then + if [ -n "${{ inputs.url }}" ]; then + COMMAND="$COMMAND ${{ inputs.url }}" + else + echo "Error: URL is required for the ${{ inputs.command }} command." + exit 1 + fi + fi + + ## Add uuid input if needed + if [ "${{ inputs.command }}" = "cancel" ] || [ "${{ inputs.command }}" = "info" ] || [ "${{ inputs.command }}" = "status" ] || [ "${{ inputs.command }}" = "success" ] || [ "${{ inputs.command }}" = "await" ]; then + if [ -n "${{ inputs.uuid }}" ]; then + COMMAND="$COMMAND ${{ inputs.uuid }}" + else + echo "Error: UUID is required for the ${{ inputs.command }} command." + exit 1 + fi + fi + + # Run + echo "[ETHIACK-JOB-MANAGER] Executing command: $COMMAND" + RESPONSE=$($COMMAND) + EXIT_CODE=$? + echo "[ETHIACK-JOB-MANAGER] Response:" + echo "$RESPONSE" + echo "response<> $GITHUB_OUTPUT + echo "[ETHIACK-JOB-MANAGER] Finished." + + set -e # Re-enable immediate exit on error + if [ $EXIT_CODE -ne 0 ]; then + echo "[ETHIACK-JOB-MANAGER] Command failed with exit code $EXIT_CODE." + exit $EXIT_CODE + fi + shell: bash diff --git a/assets/logo.webp b/assets/logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..791bb6d2353ca55e56022255ebcbbcff721664b0 GIT binary patch literal 4754 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 literal 0 HcmV?d00001