|
| 1 | +name: "Twyn action" |
| 2 | +description: "Security tool against dependency typosquatting attacks" |
| 3 | +author: "Elements Interactive" |
| 4 | + |
| 5 | +branding: |
| 6 | + icon: "search" |
| 7 | + color: "blue" |
| 8 | + |
| 9 | +inputs: |
| 10 | + github-token: |
| 11 | + description: "Token needed to publish results to the PR." |
| 12 | + required: false |
| 13 | + |
| 14 | + config: |
| 15 | + description: "Path to the config file" |
| 16 | + required: false |
| 17 | + |
| 18 | + dependency-file: |
| 19 | + description: "Dependency file(s) to analyze. Comma-separated if multiple. By default, twyn will search in the current directory for supported files, but this option will override that behavior." |
| 20 | + required: false |
| 21 | + |
| 22 | + selector-method: |
| 23 | + description: "Which method twyn should use to select possible typosquats. 'first-letter' only compares dependencies that share the first letter, 'nearby-letter' compares against dependencies whose first letter is nearby in an English keyboard. 'all' compares the given dependencies against all of those in the reference." |
| 24 | + required: false |
| 25 | + |
| 26 | + no-track: |
| 27 | + description: "Do not show the progress bar while processing packages" |
| 28 | + required: false |
| 29 | + default: "false" |
| 30 | + |
| 31 | + json: |
| 32 | + description: "Display the results in json format. It implies no-track." |
| 33 | + required: false |
| 34 | + default: "false" |
| 35 | + |
| 36 | + table: |
| 37 | + description: "Display the results in a table format. It implies no-track." |
| 38 | + required: false |
| 39 | + default: "false" |
| 40 | + |
| 41 | + recursive: |
| 42 | + description: "Recursively look for files when trying to locate them automatically. Ignored if dependency-file is given." |
| 43 | + required: false |
| 44 | + default: "false" |
| 45 | + |
| 46 | + pypi-source: |
| 47 | + description: "Alternative PyPI source URL to use for fetching trusted packages" |
| 48 | + required: false |
| 49 | + |
| 50 | + npm-source: |
| 51 | + description: "Alternative npm source URL to use for fetching trusted packages" |
| 52 | + required: false |
| 53 | + |
| 54 | + v: |
| 55 | + description: "Enable verbose output (-v)" |
| 56 | + required: false |
| 57 | + default: "false" |
| 58 | + |
| 59 | + vv: |
| 60 | + description: "Enable extra verbose output (-vv)" |
| 61 | + required: false |
| 62 | + default: "false" |
| 63 | + |
| 64 | + version: |
| 65 | + description: "Twyn version (latest, v1.0.0, etc.)" |
| 66 | + required: false |
| 67 | + default: "latest" |
| 68 | + |
| 69 | + publish: |
| 70 | + description: "Whether to publish the twyn results as PR comments (requires table format)" |
| 71 | + required: false |
| 72 | + default: "false" |
| 73 | + |
| 74 | +runs: |
| 75 | + using: "composite" |
| 76 | + steps: |
| 77 | + - name: Run Twyn Security Check |
| 78 | + shell: bash |
| 79 | + run: | |
| 80 | + # Build arguments as an array for safety (avoids word-splitting issues) |
| 81 | + ARGS=() |
| 82 | +
|
| 83 | + # Optional config file |
| 84 | + if [ -n "${{ inputs.config }}" ]; then |
| 85 | + ARGS+=(--config "${{ inputs.config }}") |
| 86 | + fi |
| 87 | +
|
| 88 | + # Dependency files (multiple allowed) |
| 89 | + if [ -n "${{ inputs.dependency-file }}" ]; then |
| 90 | + IFS=',' read -ra DEPENDENCY_FILES <<< "${{ inputs.dependency-file }}" |
| 91 | + for file in "${DEPENDENCY_FILES[@]}"; do |
| 92 | + if [ -n "$file" ]; then |
| 93 | + ARGS+=(--dependency-file "$file") |
| 94 | + fi |
| 95 | + done |
| 96 | + fi |
| 97 | +
|
| 98 | + # Selector method |
| 99 | + if [ -n "${{ inputs.selector-method }}" ]; then |
| 100 | + ARGS+=(--selector-method "${{ inputs.selector-method }}") |
| 101 | + fi |
| 102 | +
|
| 103 | + # Boolean flags |
| 104 | +
|
| 105 | + if [ "${{ inputs.no-track }}" = "true" ]; then |
| 106 | + ARGS+=(--no-track) |
| 107 | + fi |
| 108 | +
|
| 109 | + if [ "${{ inputs.json }}" = "true" ]; then |
| 110 | + ARGS+=(--json) |
| 111 | + fi |
| 112 | +
|
| 113 | + # Force table format when publishing |
| 114 | + if [ "${{ inputs.publish }}" = "true" ] || [ "${{ inputs.table }}" = "true" ]; then |
| 115 | + ARGS+=(--table) |
| 116 | + fi |
| 117 | +
|
| 118 | + if [ "${{ inputs.recursive }}" = "true" ]; then |
| 119 | + ARGS+=(--recursive) |
| 120 | + fi |
| 121 | +
|
| 122 | + # Source URLs |
| 123 | + if [ -n "${{ inputs.pypi-source }}" ]; then |
| 124 | + ARGS+=(--pypi-source "${{ inputs.pypi-source }}") |
| 125 | + fi |
| 126 | +
|
| 127 | + if [ -n "${{ inputs.npm-source }}" ]; then |
| 128 | + ARGS+=(--npm-source "${{ inputs.npm-source }}") |
| 129 | + fi |
| 130 | +
|
| 131 | +
|
| 132 | + # Run twyn using Docker and capture output and exit code |
| 133 | + # Use 'set +e' to prevent script from exiting on non-zero exit codes |
| 134 | + set +e |
| 135 | + TWYN_OUTPUT=$(docker run --rm \ |
| 136 | + -v "${{ github.workspace }}:/workspace" \ |
| 137 | + -w /workspace \ |
| 138 | + elementsinteractive/twyn:${{ inputs.version }} run \ |
| 139 | + "${ARGS[@]}" 2>/dev/null) |
| 140 | + TWYN_EXIT_CODE=$? |
| 141 | + set -e |
| 142 | +
|
| 143 | + # Display output in action logs |
| 144 | + echo "$TWYN_OUTPUT" |
| 145 | +
|
| 146 | + # Create PR comment with twyn results |
| 147 | + if [ "${{ inputs.publish }}" = "true" ]; then |
| 148 | + # Check if github-token is provided |
| 149 | + if [ -z "${{ inputs.github-token }}" ]; then |
| 150 | + echo "❌ Error: github-token is required when publish is enabled. Skipping..." |
| 151 | + else |
| 152 | + echo "Publishing" |
| 153 | + |
| 154 | + # Create comment content |
| 155 | + echo "$TWYN_OUTPUT" >> comment.md |
| 156 | + |
| 157 | + curl -X POST \ |
| 158 | + -H "Authorization: token ${{ inputs.github-token}}" \ |
| 159 | + -H "Accept: application/vnd.github.v3+json" \ |
| 160 | + -d "$(cat comment.md | jq -Rs '{"body": .}')" \ |
| 161 | + "https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" |
| 162 | + |
| 163 | + if [ $? -eq 0 ]; then |
| 164 | + echo "✅ Successfully posted comment to PR" |
| 165 | + else |
| 166 | + echo "❌ Failed to post comment to PR" |
| 167 | + fi |
| 168 | + fi |
| 169 | +
|
| 170 | + else |
| 171 | + echo "ℹ️ Publish to PR is disabled (publish: ${{ inputs.publish }})" |
| 172 | + fi |
| 173 | +
|
| 174 | + # Set final exit code for the action |
| 175 | + # Exit with 0 if we're just reporting findings (exit code 1) |
| 176 | + # Exit with the actual code for real errors (exit codes > 1) |
| 177 | + if [ $TWYN_EXIT_CODE -gt 1 ]; then |
| 178 | + exit $TWYN_EXIT_CODE |
| 179 | + else |
| 180 | + exit 0 |
| 181 | + fi |
0 commit comments