Skip to content

Commit ac6775d

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

File tree

4 files changed

+264
-0
lines changed

4 files changed

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

action.yml

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
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+
config:
11+
description: "Path to the config file"
12+
required: false
13+
14+
dependency-file:
15+
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."
16+
required: false
17+
18+
selector-method:
19+
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."
20+
required: false
21+
22+
no-track:
23+
description: "Do not show the progress bar while processing packages"
24+
required: false
25+
default: "false"
26+
27+
json:
28+
description: "Display the results in json format. It implies no-track."
29+
required: false
30+
default: "false"
31+
32+
table:
33+
description: "Display the results in a table format. It implies no-track."
34+
required: false
35+
default: "false"
36+
37+
recursive:
38+
description: "Recursively look for files when trying to locate them automatically. Ignored if dependency-file is given."
39+
required: false
40+
default: "false"
41+
42+
pypi-source:
43+
description: "Alternative PyPI source URL to use for fetching trusted packages"
44+
required: false
45+
46+
npm-source:
47+
description: "Alternative npm source URL to use for fetching trusted packages"
48+
required: false
49+
50+
v:
51+
description: "Enable verbose output (-v)"
52+
required: false
53+
default: "false"
54+
55+
vv:
56+
description: "Enable extra verbose output (-vv)"
57+
required: false
58+
default: "false"
59+
60+
version:
61+
description: "Twyn version (latest, v1.0.0, etc.)"
62+
required: false
63+
default: "latest"
64+
65+
publish:
66+
description: "Whether to publish the twyn results as PR comments (requires table format)"
67+
required: false
68+
default: "false"
69+
70+
runs:
71+
using: "composite"
72+
steps:
73+
- name: Run Twyn Security Check
74+
shell: bash
75+
run: |
76+
# Build arguments as an array for safety (avoids word-splitting issues)
77+
ARGS=()
78+
79+
# Optional config file
80+
if [ -n "${{ inputs.config }}" ]; then
81+
ARGS+=(--config "${{ inputs.config }}")
82+
fi
83+
84+
# Dependency files (multiple allowed)
85+
if [ -n "${{ inputs.dependency-file }}" ]; then
86+
IFS=',' read -ra DEPENDENCY_FILES <<< "${{ inputs.dependency-file }}"
87+
for file in "${DEPENDENCY_FILES[@]}"; do
88+
if [ -n "$file" ]; then
89+
ARGS+=(--dependency-file "$file")
90+
fi
91+
done
92+
fi
93+
94+
# Dependencies (multiple allowed)
95+
if [ -n "${{ inputs.dependency }}" ]; then
96+
IFS=',' read -ra DEPENDENCIES <<< "${{ inputs.dependency }}"
97+
for dep in "${DEPENDENCIES[@]}"; do
98+
if [ -n "$dep" ]; then
99+
ARGS+=(--dependency "$dep")
100+
fi
101+
done
102+
fi
103+
104+
# Selector method
105+
if [ -n "${{ inputs.selector-method }}" ]; then
106+
ARGS+=(--selector-method "${{ inputs.selector-method }}")
107+
fi
108+
109+
# Package ecosystem
110+
if [ -n "${{ inputs.package-ecosystem }}" ]; then
111+
ARGS+=(--package-ecosystem "${{ inputs.package-ecosystem }}")
112+
fi
113+
114+
# Boolean flags
115+
if [ "${{ inputs.no-cache }}" = "true" ]; then
116+
ARGS+=(--no-cache)
117+
fi
118+
119+
if [ "${{ inputs.no-track }}" = "true" ]; then
120+
ARGS+=(--no-track)
121+
fi
122+
123+
if [ "${{ inputs.json }}" = "true" ]; then
124+
ARGS+=(--json)
125+
fi
126+
127+
# Force table format when publishing
128+
if [ "${{ inputs.publish }}" = "true" ] || [ "${{ inputs.table }}" = "true" ]; then
129+
ARGS+=(--table)
130+
fi
131+
132+
if [ "${{ inputs.recursive }}" = "true" ]; then
133+
ARGS+=(--recursive)
134+
fi
135+
136+
# Source URLs
137+
if [ -n "${{ inputs.pypi-source }}" ]; then
138+
ARGS+=(--pypi-source "${{ inputs.pypi-source }}")
139+
fi
140+
141+
if [ -n "${{ inputs.npm-source }}" ]; then
142+
ARGS+=(--npm-source "${{ inputs.npm-source }}")
143+
fi
144+
145+
# Verbose mode
146+
if [ "${{ inputs.verbose }}" = "true" ]; then
147+
ARGS+=(-vv)
148+
fi
149+
150+
echo $ARGS
151+
152+
# Run twyn using Docker and capture output and exit code
153+
# Use 'set +e' to prevent script from exiting on non-zero exit codes
154+
set +e
155+
TWYN_OUTPUT=$(docker run --rm \
156+
-v "${{ github.workspace }}:/workspace" \
157+
-w /workspace \
158+
elementsinteractive/twyn:${{ inputs.version }} run \
159+
"${ARGS[@]}" 2>&1)
160+
TWYN_EXIT_CODE=$?
161+
set -e
162+
163+
# Also display output in action logs
164+
echo "$TWYN_OUTPUT"
165+
166+
# Create PR comment with twyn results
167+
if [ "${{ inputs.publish }}" = "true" ] && [ -n "$GITHUB_TOKEN" ] && [ -n "${{ github.event.pull_request.number }}" ]; then
168+
echo "Publishing results to PR #${{ github.event.pull_request.number }}"
169+
170+
# Determine status emoji based on exit code
171+
if [ $TWYN_EXIT_CODE -eq 0 ]; then
172+
STATUS_EMOJI="✅"
173+
STATUS_TEXT="No issues found"
174+
else
175+
STATUS_EMOJI="⚠️"
176+
STATUS_TEXT="Potential issues detected"
177+
fi
178+
179+
cat > comment.md << EOF
180+
## $STATUS_EMOJI Twyn Security Check Results
181+
182+
**Status:** $STATUS_TEXT
183+
184+
\`\`\`
185+
$TWYN_OUTPUT
186+
\`\`\`
187+
188+
echo "Publishing results"
189+
190+
---
191+
*This comment was automatically generated by the Twyn security action.*
192+
EOF
193+
194+
curl -X POST \
195+
-H "Authorization: token $GITHUB_TOKEN" \
196+
-H "Accept: application/vnd.github.v3+json" \
197+
-d "$(cat comment.md | jq -Rs '{"body": .}')" \
198+
"https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments"
199+
else
200+
echo "⚠️ Cannot publish to PR: publish not enabled, GITHUB_TOKEN missing, or not in a PR context"
201+
fi
202+
203+
# Set final exit code for the action
204+
# Exit with 0 if we're just reporting findings (exit code 1)
205+
# Exit with the actual code for real errors (exit codes > 1)
206+
if [ $TWYN_EXIT_CODE -gt 1 ]; then
207+
exit $TWYN_EXIT_CODE
208+
else
209+
exit 0
210+
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)