-
-
Notifications
You must be signed in to change notification settings - Fork 5
286 lines (249 loc) · 10.8 KB
/
onboard-new-repo.yml
File metadata and controls
286 lines (249 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
name: 'Onboard New Repository with SAST'
on:
workflow_dispatch:
inputs:
organization:
description: 'Organization name (e.g., MetaMask)'
required: true
type: string
repository:
description: 'Repository name (e.g., snaps)'
required: true
type: string
repository_dispatch:
types: [new_repository_created]
jobs:
create-sast-pr:
runs-on: ubuntu-latest
environment: onboarding
permissions:
contents: read
steps:
- name: Checkout scanner action repository
uses: actions/checkout@v4
with:
path: scanner-repo
- name: Parse target repository inputs
id: target
run: |
validate_name() {
local value="$1"
local label="$2"
local max_len="$3"
if [ -z "$value" ]; then
echo "::error::$label is empty"
exit 1
fi
if [ "${#value}" -gt "$max_len" ]; then
echo "::error::$label exceeds maximum length of $max_len characters"
exit 1
fi
if ! echo "$value" | grep -qE '^[a-zA-Z0-9._-]+$'; then
echo "::error::$label contains invalid characters (only alphanumeric, dots, hyphens, and underscores are allowed)"
exit 1
fi
}
if [ "$EVENT_NAME" = "repository_dispatch" ]; then
ORG="$EVENT_ORG"
REPO_NAME="$EVENT_REPO"
else
ORG="$INPUT_ORG"
REPO_NAME="$INPUT_REPO"
fi
validate_name "$ORG" "Organization" 39
validate_name "$REPO_NAME" "Repository" 100
{
echo "organization=$ORG"
echo "repo_name=$REPO_NAME"
echo "repository=$ORG/$REPO_NAME"
} >> "$GITHUB_OUTPUT"
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
EVENT_ORG: ${{ github.event.client_payload.organization }}
EVENT_REPO: ${{ github.event.client_payload.repository }}
INPUT_ORG: ${{ inputs.organization }}
INPUT_REPO: ${{ inputs.repository }}
- name: Generate GitHub App token
id: app_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.ONBOARDING_APP_ID }}
private-key: ${{ secrets.ONBOARDING_APP_PRIVATE_KEY }}
owner: ${{ steps.target.outputs.organization }}
repositories: ${{ steps.target.outputs.repo_name }}
- name: Detect default branch
id: detect_branch
run: |
echo "Detecting default branch for $REPO..."
BASE_BRANCH=$(gh api "repos/$REPO" --jq '.default_branch' 2>/dev/null) || BASE_BRANCH=""
if [ -z "$BASE_BRANCH" ] || [ "$BASE_BRANCH" = "null" ]; then
echo "Repository is empty or default branch not found. Defaulting to 'main'"
BASE_BRANCH="main"
fi
if ! echo "$BASE_BRANCH" | grep -qE '^[a-zA-Z0-9._/-]+$'; then
echo "::error::Branch name contains invalid characters (only alphanumeric, dots, hyphens, slashes, and underscores are allowed)"
exit 1
fi
echo "base_branch=$BASE_BRANCH" >> "$GITHUB_OUTPUT"
shell: bash
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
REPO: ${{ steps.target.outputs.repository }}
- name: Check for opt-out file
id: check_opt_out
run: |
if gh api "repos/$REPO/contents/.github/no-security-scanner?ref=$BASE_BRANCH" > /dev/null 2>&1; then
echo "Repository has opted out via .github/no-security-scanner"
echo "opted_out=true" >> "$GITHUB_OUTPUT"
else
echo "opted_out=false" >> "$GITHUB_OUTPUT"
fi
shell: bash
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
REPO: ${{ steps.target.outputs.repository }}
BASE_BRANCH: ${{ steps.detect_branch.outputs.base_branch }}
- name: Skip onboarding (repository opted out)
if: steps.check_opt_out.outputs.opted_out == 'true'
run: |
echo "::notice::Skipping onboarding — repository $REPO has a .github/no-security-scanner opt-out file"
env:
REPO: ${{ steps.target.outputs.repository }}
- name: Check if target repository is empty
if: steps.check_opt_out.outputs.opted_out != 'true'
id: check_empty
run: |
if ! BRANCHES=$(gh api "repos/$REPO/branches" --jq 'length' 2>&1); then
echo "::error::GitHub API call failed while listing branches for $REPO: $BRANCHES"
exit 1
fi
if ! echo "$BRANCHES" | grep -qE '^[0-9]+$'; then
echo "::error::Unexpected API response when listing branches for $REPO: $BRANCHES"
exit 1
fi
if [ "$BRANCHES" = "0" ]; then
IS_EMPTY="true"
echo "Repository is empty (no branches found)"
else
IS_EMPTY="false"
echo "Repository has $BRANCHES branch(es)"
fi
echo "is_empty=$IS_EMPTY" >> "$GITHUB_OUTPUT"
shell: bash
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
REPO: ${{ steps.target.outputs.repository }}
- name: Checkout target repository
if: steps.check_opt_out.outputs.opted_out != 'true' && steps.check_empty.outputs.is_empty == 'false'
uses: actions/checkout@v4
with:
repository: ${{ steps.target.outputs.repository }}
token: ${{ steps.app_token.outputs.token }}
path: target-repo
ref: ${{ steps.detect_branch.outputs.base_branch }}
- name: Initialize empty repository locally
if: steps.check_opt_out.outputs.opted_out != 'true' && steps.check_empty.outputs.is_empty == 'true'
run: |
mkdir -p target-repo
cd target-repo
git init
git remote add origin "https://x-access-token:${APP_TOKEN}@github.com/${REPO}.git"
shell: bash
env:
APP_TOKEN: ${{ steps.app_token.outputs.token }}
REPO: ${{ steps.target.outputs.repository }}
- name: Create branch and add SAST workflow
id: create_branch
if: steps.check_opt_out.outputs.opted_out != 'true'
working-directory: target-repo
env:
IS_EMPTY: ${{ steps.check_empty.outputs.is_empty }}
BASE_BRANCH: ${{ steps.detect_branch.outputs.base_branch }}
run: |
git config user.name "MetaMask Security Bot"
git config user.email "security-bot@metamask.io"
if [ "$IS_EMPTY" = "true" ]; then
# For empty repos, create initial commit on main
BRANCH_NAME="$BASE_BRANCH"
else
# For existing repos, create a feature branch
BRANCH_NAME="security/add-sast-scanner"
git checkout -b "$BRANCH_NAME"
fi
mkdir -p .github/workflows
sed "s|{ DEFAULT_BRANCH }|$BASE_BRANCH|g" \
../scanner-repo/.github/templates/security-code-scanner.yml \
> .github/workflows/security-code-scanner.yml
git add .github/workflows/security-code-scanner.yml
if git diff --cached --quiet; then
echo "::notice::Workflow file already exists and matches — nothing to commit"
echo "skipped=true" >> "$GITHUB_OUTPUT"
exit 0
fi
git commit -m "chore: add MetaMask Security Code Scanner workflow
This PR adds the MetaMask Security Code Scanner workflow to enable
automated security scanning of the codebase.
The scanner will run on:
- Push to $BASE_BRANCH branch
- Pull requests to $BASE_BRANCH branch
- Manual workflow dispatch
To configure the scanner for your repository's specific needs,
please review the workflow file and adjust as necessary."
if [ "$IS_EMPTY" = "true" ]; then
git branch -M "$BRANCH_NAME"
fi
git push -u origin "$BRANCH_NAME"
echo "skipped=false" >> "$GITHUB_OUTPUT"
shell: bash
- name: Create Pull Request
if: steps.check_opt_out.outputs.opted_out != 'true' && steps.check_empty.outputs.is_empty == 'false' && steps.create_branch.outputs.skipped != 'true'
working-directory: target-repo
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
REPO_NAME: ${{ steps.target.outputs.repository }}
BASE_BRANCH: ${{ steps.detect_branch.outputs.base_branch }}
run: |
# Extract owner and repo name for URL construction
OWNER=$(echo "$REPO_NAME" | cut -d'/' -f1)
REPO=$(echo "$REPO_NAME" | cut -d'/' -f2)
SECURITY_URL="https://github.com/${OWNER}/${REPO}/security/code-scanning"
# Read PR body template and substitute variables
PR_BODY=$(cat ../scanner-repo/.github/templates/onboarding-pr-body-automated.md)
PR_BODY="${PR_BODY//\{\{SECURITY_SCANNING_URL\}\}/$SECURITY_URL}"
gh pr create \
--title "🔒 Add MetaMask Security Code Scanner" \
--body "$PR_BODY" \
--base "$BASE_BRANCH" \
--head "security/add-sast-scanner"
shell: bash
- name: Output PR URL
if: steps.check_opt_out.outputs.opted_out != 'true' && steps.check_empty.outputs.is_empty == 'false' && steps.create_branch.outputs.skipped != 'true'
working-directory: target-repo
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
PR_URL=$(gh pr view security/add-sast-scanner --json url -q .url)
echo "✅ Pull Request created: $PR_URL"
echo "PR_URL=$PR_URL" >> "$GITHUB_OUTPUT"
shell: bash
- name: Output commit info for empty repo
if: steps.check_opt_out.outputs.opted_out != 'true' && steps.check_empty.outputs.is_empty == 'true' && steps.create_branch.outputs.skipped != 'true'
run: |
echo "✅ Initial commit pushed to https://github.com/$REPO/tree/$BASE_BRANCH"
echo "Repository was empty - workflow file added directly to $BASE_BRANCH branch"
shell: bash
env:
REPO: ${{ steps.target.outputs.repository }}
BASE_BRANCH: ${{ steps.detect_branch.outputs.base_branch }}
- name: Post to Slack channel on failure
if: ${{ failure() && env.SLACK_WEBHOOK_URL != '' }}
uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
with:
payload: |
{
"text": "Onboarding failed for ${{ steps.target.outputs.repository }} - Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.APPSEC_BOT_SLACK_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK