Skip to content

Commit ebb38d2

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

File tree

4 files changed

+296
-0
lines changed

4 files changed

+296
-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: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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 }}
24+
security-scan:
25+
runs-on: ubuntu-latest
26+
outputs:
27+
# Expose outputs from this job to other jobs
28+
scan-results: ${{ steps.twyn.outputs.results }}
29+
has-findings: ${{ steps.twyn.outputs.has-findings }}
30+
exit-code: ${{ steps.twyn.outputs.exit-code }}
31+
steps:
32+
- uses: actions/checkout@v4
33+
34+
- name: Run Twyn Security Scan
35+
id: twyn
36+
uses: ./
37+
with:
38+
json: true # Get JSON output for parsing
39+
dependency-file: requirements.txt
40+
41+
- name: Show results in this job
42+
run: |
43+
echo "Exit code: ${{ steps.twyn.outputs.exit-code }}"
44+
echo "Has findings: ${{ steps.twyn.outputs.has-findings }}"
45+
echo "Results: ${{ steps.twyn.outputs.results }}"

action.yml

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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+
outputs:
75+
results:
76+
description: "Raw output from twyn scan"
77+
value: ${{ steps.twyn-scan.outputs.results }}
78+
exit-code:
79+
description: "Exit code from twyn scan (0=no issues, 1=issues found, >1=error)"
80+
value: ${{ steps.twyn-scan.outputs.exit-code }}
81+
has-findings:
82+
description: "Boolean indicating if twyn found any issues (true/false)"
83+
value: ${{ steps.twyn-scan.outputs.has-findings }}
84+
85+
runs:
86+
using: "composite"
87+
steps:
88+
- name: Run Twyn Security Check
89+
id: twyn-scan
90+
shell: bash
91+
run: |
92+
# Build arguments as an array for safety (avoids word-splitting issues)
93+
ARGS=()
94+
95+
# Optional config file
96+
if [ -n "${{ inputs.config }}" ]; then
97+
ARGS+=(--config "${{ inputs.config }}")
98+
fi
99+
100+
# Dependency files (multiple allowed)
101+
if [ -n "${{ inputs.dependency-file }}" ]; then
102+
IFS=',' read -ra DEPENDENCY_FILES <<< "${{ inputs.dependency-file }}"
103+
for file in "${DEPENDENCY_FILES[@]}"; do
104+
if [ -n "$file" ]; then
105+
ARGS+=(--dependency-file "$file")
106+
fi
107+
done
108+
fi
109+
110+
# Selector method
111+
if [ -n "${{ inputs.selector-method }}" ]; then
112+
ARGS+=(--selector-method "${{ inputs.selector-method }}")
113+
fi
114+
115+
# Boolean flags
116+
117+
if [ "${{ inputs.no-track }}" = "true" ]; then
118+
ARGS+=(--no-track)
119+
fi
120+
121+
if [ "${{ inputs.json }}" = "true" ]; then
122+
ARGS+=(--json)
123+
fi
124+
125+
# Force table format when publishing
126+
if [ "${{ inputs.publish }}" = "true" ] || [ "${{ inputs.table }}" = "true" ]; then
127+
ARGS+=(--table)
128+
fi
129+
130+
if [ "${{ inputs.recursive }}" = "true" ]; then
131+
ARGS+=(--recursive)
132+
fi
133+
134+
# Source URLs
135+
if [ -n "${{ inputs.pypi-source }}" ]; then
136+
ARGS+=(--pypi-source "${{ inputs.pypi-source }}")
137+
fi
138+
139+
if [ -n "${{ inputs.npm-source }}" ]; then
140+
ARGS+=(--npm-source "${{ inputs.npm-source }}")
141+
fi
142+
143+
# Verbose mode
144+
if [ "${{ inputs.vv }}" = "true" ]; then
145+
ARGS+=(-vv)
146+
elif [ "${{ inputs.v }}" = "true" ]; then
147+
ARGS+=(-v)
148+
fi
149+
150+
151+
# Run twyn using Docker and capture output and exit code
152+
# Use 'set +e' to prevent script from exiting on non-zero exit codes
153+
set +e
154+
TWYN_OUTPUT=$(docker run --rm \
155+
-v "${{ github.workspace }}:/workspace" \
156+
-w /workspace \
157+
elementsinteractive/twyn:${{ inputs.version }} run \
158+
"${ARGS[@]}" 2>/dev/null)
159+
TWYN_EXIT_CODE=$?
160+
set -e
161+
162+
# Display output in action logs
163+
echo "$TWYN_OUTPUT"
164+
165+
# Set action outputs
166+
echo "results<<EOF" >> $GITHUB_OUTPUT
167+
echo "$TWYN_OUTPUT" >> $GITHUB_OUTPUT
168+
echo "EOF" >> $GITHUB_OUTPUT
169+
170+
echo "exit-code=$TWYN_EXIT_CODE" >> $GITHUB_OUTPUT
171+
172+
# Set has-findings based on exit code
173+
if [ $TWYN_EXIT_CODE -eq 1 ]; then
174+
echo "has-findings=true" >> $GITHUB_OUTPUT
175+
else
176+
echo "has-findings=false" >> $GITHUB_OUTPUT
177+
fi
178+
179+
# Create PR comment with twyn results
180+
if [ "${{ inputs.publish }}" = "true" ]; then
181+
# Check if github-token is provided
182+
if [ -z "${{ inputs.github-token }}" ]; then
183+
echo "❌ Error: github-token is required when publish is enabled. Skipping..."
184+
exit $TWYN_EXIT_CODE
185+
else
186+
187+
# Create comment content with proper formatting
188+
echo "## 🛡️ Twyn Security Check Results" > comment.md
189+
echo "" >> comment.md
190+
echo '```' >> comment.md
191+
# Process the output to handle escape sequences properly
192+
echo "$TWYN_OUTPUT" | sed 's/\\n/\n/g' >> comment.md
193+
echo '```' >> comment.md
194+
195+
curl -X POST \
196+
-H "Authorization: token ${{ inputs.github-token}}" \
197+
-H "Accept: application/vnd.github.v3+json" \
198+
-d "$(cat comment.md | jq -Rs '{"body": .}')" \
199+
"https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments"
200+
201+
if [ $? -eq 0 ]; then
202+
echo "✅ Successfully posted comment to PR"
203+
else
204+
echo "❌ Failed to post comment to PR"
205+
fi
206+
fi
207+
208+
else
209+
echo "ℹ️ Publish to PR is disabled (publish: ${{ inputs.publish }})"
210+
fi
211+
212+
# Set final exit code for the action
213+
# Exit with 0 if we're just reporting findings (exit code 1)
214+
# Exit with the actual code for real errors (exit codes > 1)
215+
if [ $TWYN_EXIT_CODE -gt 1 ]; then
216+
exit $TWYN_EXIT_CODE
217+
else
218+
exit 0
219+
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)