Skip to content

Commit aeac8e6

Browse files
committed
feat: initial version
1 parent 2473cf2 commit aeac8e6

File tree

4 files changed

+236
-0
lines changed

4 files changed

+236
-0
lines changed

.github/workflows/lint.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# This workflow will check our code for having a proper format, as well as the commit message to meet the expected ones
2+
3+
name: Lint
4+
5+
on:
6+
push:
7+
branches: ["main"]
8+
pull_request:
9+
branches: ["main"]
10+
11+
jobs:
12+
lint-commit:
13+
runs-on: ubuntu-latest
14+
name: "Lint commit message"
15+
container:
16+
image: commitizen/commitizen:4.8.3@sha256:08a078c52b368f85f34257a66e10645ee74d8cbe9b471930b80b2b4e95a9bd4a
17+
steps:
18+
- name: Check out
19+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
20+
- name: Check commit message
21+
run: |
22+
git config --global --add safe.directory .
23+
cz check --rev-range HEAD

.github/workflows/test-action.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Test Twyn Action
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
push:
7+
branches: [main]
8+
workflow_dispatch:
9+
10+
jobs:
11+
test-action:
12+
runs-on: ubuntu-latest
13+
name: Test GH action
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
17+
18+
- name: Test Twyn Action
19+
uses: ./
20+
with:
21+
vv: true
22+
publish: "true"
23+
github-token: ${{ secrets.GITHUB_TOKEN }}

action.yml

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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

requirements.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Test files for Twyn Action
2+
3+
# Python requirements with potential typosquats
4+
requests==2.28.0
5+
nmupy==1.24.0 # typo: numpy
6+
pandsa==1.5.0 # typo: pandas
7+
falsk==2.2.0 # typo: flask
8+
djagno==4.1.0 # typo: django
9+
beatifulsoup4==4.11.0 # typo: beautifulsoup4

0 commit comments

Comments
 (0)