Skip to content

Commit 5290a41

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

File tree

8 files changed

+348
-0
lines changed

8 files changed

+348
-0
lines changed

.github/release.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
changelog:
2+
exclude:
3+
labels:
4+
- ignore-for-release
5+
authors:
6+
- octocat
7+
categories:
8+
- title: Breaking Changes 🛠
9+
labels:
10+
- breaking
11+
- title: New Features 🎉
12+
labels:
13+
- feature
14+
- title: Fixes 🔧
15+
labels:
16+
- fix
17+
- title: Other Changes
18+
labels:
19+
- "*"

.github/workflows/bump-version.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Bump version
2+
3+
on:
4+
workflow_dispatch:
5+
jobs:
6+
bump_version:
7+
if: "!startsWith(github.event.head_commit.message, 'bump:') && github.ref == 'refs/heads/main'"
8+
runs-on: ubuntu-latest
9+
name: "Bump version and create changelog with commitizen"
10+
steps:
11+
- uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
12+
id: app-token
13+
with:
14+
app-id: ${{ vars.ELEMENTSINTERACTIVE_BOT_APP_ID }}
15+
private-key: ${{ secrets.ELEMENTSINTERACTIVE_BOT_PRIVATE_KEY }}
16+
- uses: actions/checkout@@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
17+
with:
18+
fetch-depth: 0
19+
token: ${{ steps.app-token.outputs.token }}
20+
ref: ${{ github.head_ref }}
21+
# Make sure the value of GITHUB_TOKEN will not be persisted in repo's config
22+
persist-credentials: false
23+
- id: cz
24+
name: Create bump and changelog
25+
uses: commitizen-tools/commitizen-action@5b0848cd060263e24602d1eba03710e056ef7711 # v0.24.0
26+
with:
27+
github_token: ${{ steps.app-token.outputs.token }}
28+
- name: Print Version
29+
run: echo "Bumped to version ${{ steps.cz.outputs.version }}"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
on:
2+
pull_request_target:
3+
branches: ["main"]
4+
5+
name: conventional-release-labels
6+
jobs:
7+
label:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: bcoe/conventional-release-labels@886f696738527c7be444262c327c89436dfb95a8 #v1.3.1
11+
with:
12+
type_labels: '{"feat": "feature", "fix": "fix", "BREAKING CHANGE": "breaking", "ci": "CI", "build": "build", "refactor": "refactor", "test": "test"}'

.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/publish.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Publish
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*.*.*"
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
release:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
16+
17+
- name: Release
18+
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
19+
with:
20+
generate_release_notes: true
21+
make_latest: true
22+
token: "${{ secrets.GITHUB_TOKEN }}"

CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@sdn4z @scastlara

action.yml

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