Skip to content

Commit 1feafd2

Browse files
authored
test(Snyk): security agent testings (#607)
2 parents 0b9fed5 + e9dfcc9 commit 1feafd2

15 files changed

Lines changed: 223 additions & 38 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../.github/skills/component-authoring
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Snyk Agent Scan
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
snyk-agent-scan:
13+
name: Snyk Agent Scan
14+
runs-on: ubuntu-22.04
15+
continue-on-error: true
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v4
19+
with:
20+
fetch-depth: 0
21+
22+
- name: Setup pnpm
23+
uses: pnpm/action-setup@v4
24+
25+
- name: Setup Node
26+
uses: actions/setup-node@v4
27+
with:
28+
node-version: "22.18.0"
29+
cache: "pnpm"
30+
31+
- name: Install dependencies
32+
run: pnpm install --frozen-lockfile
33+
34+
- name: Install uv
35+
uses: astral-sh/setup-uv@v7
36+
37+
- name: Run Snyk Agent Scan
38+
env:
39+
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
40+
GIT_BASE_SHA: ${{ github.event.before }}
41+
run: pnpm scan:snyk-agent

.storybook/main.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module.exports = {
66
"../stories/migration/**/*.@(mdx|stories.@(js|jsx|ts|tsx))",
77
"../stories/frameworks/**/*.@(mdx|stories.@(js|jsx|ts|tsx))",
88
"../stories/getting-started/**/*.@(mdx|stories.@(js|jsx|ts|tsx))",
9+
"../stories/agent-skills/!(*MCPServer*).@(mdx|stories.@(js|jsx|ts|tsx))",
910
"../stories/components/!(*Sidebar*).@(mdx|stories.@(js|jsx|ts|tsx))",
1011
"../stories/utilities/**/*.@(mdx|stories.@(js|jsx|ts|tsx))",
1112
"../stories/form-validation/*.@(mdx|stories.@(js|jsx|ts|tsx))",

.vscode/settings.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
"workbench.colorCustomizations": {
33
"activityBar.background": "#203220",
44
"titleBar.activeBackground": "#2C452D",
5-
"titleBar.activeForeground": "#FAFCFA"
65
},
76
"todo-tree.highlights.customHighlight": {
87
"TODO": {

docs/AgentSkills.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,19 @@ Once installed, your AI assistant can answer questions like:
1616

1717
Run the following command in your project root:
1818

19+
For Govtechies, use <a href="https://software-engineering.gto.tech.gov.sg/handbook/how-to/local-development/developer-cli/" target="_blank">GovTech Developer CLI</a> to install
20+
1921
```bash
20-
npx skills add govtechsg/sgds-web-component
22+
gt apd install sgds
2123
```
2224

23-
Select all existing skills from the list. This pulls the latest skills from the `skills/` folder of this repository into your local `.agents/` directory, where compatible AI tools automatically pick them up.
25+
<!-- For others,
26+
27+
```bash
28+
npx skills add govtechsg/sgds-web-component
29+
``` -->
30+
31+
<!-- Select all existing skills from the list. This pulls the latest skills from the `skills/` folder of this repository into your local `.agents/` directory, where compatible AI tools automatically pick them up. -->
2432

2533
---
2634

@@ -96,10 +104,16 @@ Skills are read by the agent at query time — they do not add runtime dependenc
96104

97105
Run the following command to pull the latest skill updates:
98106

107+
For Govtechies,
108+
109+
```bash
110+
gt apd install sgds
111+
```
112+
<!--
99113
```bash
100114
101115
npx skills add govtechsg/sgds-web-component
102116
103-
```
117+
``` -->
104118

105-
Skills follow the library version. After upgrading `@govtechsg/sgds-web-component`, update skills to ensure the AI agent's knowledge matches the installed version.
119+
Skills follow the latest library version. After upgrading `@govtechsg/sgds-web-component`, update skills to ensure the AI agent's knowledge matches the installed version.

docs/INSTALLATION.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ This method registers all SGDS elements up front in the Custom Elements Registry
5757
<link href='https://cdn.jsdelivr.net/npm/@govtechsg/sgds-web-component@3.16.0/css/sgds.css' rel='stylesheet' type='text/css' />
5858

5959
// it is recommended to load a particular version when using cdn e.g. https://cdn.jsdelivr.net/npm/@govtechsg/sgds-web-component@1.0.2
60-
<script src="https://cdn.jsdelivr.net/npm/@govtechsg/sgds-web-component@3.16.0" async crossorigin="anonymous" integrity="sha384-EKivL3CQ+YTcruoxwrg9bNMXdIYcM9EBmICwTMUYcbl105u0vnfurPgjWTBNORtm"></script>
60+
<script src="https://cdn.jsdelivr.net/npm/@govtechsg/sgds-web-component@3.16.0" async crossorigin="anonymous" integrity="sha384-Fac1dcR+ZUsXkpuxOsemc5Ld/Pu6/sHkOKu3fZg/ckl5nxa9o+CQ2gdETEadyjJs"></script>
6161

6262
//or load a single component e.g. Masthead
63-
<script src="https://cdn.jsdelivr.net/npm/@govtechsg/sgds-web-component@3.16.0/components/Masthead/index.umd.min.js" async crossorigin="anonymous" integrity="sha384-ITgmRQ1eDoGTKD6ytTZVquH/6jBLIuZzFPDjUVaN525LjuJl3zngwzf/hYiDgJbn"></script>
63+
<script src="https://cdn.jsdelivr.net/npm/@govtechsg/sgds-web-component@3.16.0/components/Masthead/index.umd.min.js" async crossorigin="anonymous" integrity="sha384-NTZt1tFTkbhjjHYUtbUDmLqrKG7WmyhNCm1QhFxafmpPq2e7ozALWTPLO2DhBTSP"></script>
6464

6565
```
6666

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
"generate:icons": "node scripts/generateIconRegistry.js",
3535
"chromatic": "chromatic",
3636
"utility:dev": "tailwindcss -i ./src/css/utility.css -o ./playground/css/utility.css --config tailwind.config.js --watch",
37-
"utility:storybook": "tailwindcss -i ./src/css/utility.css -o ./.storybook/utility.css --config tailwind.config.js"
37+
"utility:storybook": "tailwindcss -i ./src/css/utility.css -o ./.storybook/utility.css --config tailwind.config.js",
38+
"scan:snyk-agent": "tsx scripts/snyk-agent-scan/index.ts"
3839
},
3940
"author": "GovTechSG",
4041
"contributors": [

scripts/snyk-agent-scan/index.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import * as fs from "fs";
2+
import { glob } from "fs/promises";
3+
import { getChangedFiles } from "./utils/git.js";
4+
import { printChangedFiles, printScanHeader, printSummary } from "./utils/reporter.js";
5+
import { scanFile, type ScanResult } from "./utils/scanner.js";
6+
7+
const SCAN_PATHS = ["skills/", ".github/skills/"];
8+
9+
async function resolveFiles(): Promise<string[]> {
10+
const args = process.argv.slice(2);
11+
12+
if (args.length > 0) {
13+
const files: string[] = [];
14+
for (const pattern of args) {
15+
for await (const match of glob(pattern)) {
16+
files.push(match);
17+
}
18+
}
19+
return files;
20+
}
21+
22+
const baseSha = process.env.GIT_BASE_SHA;
23+
if (baseSha) {
24+
if (/^0+$/.test(baseSha)) {
25+
console.log("No previous commit to diff against, skipping scan.");
26+
process.exit(0);
27+
}
28+
return getChangedFiles(baseSha, SCAN_PATHS);
29+
}
30+
31+
console.error("Usage: pnpm scan:snyk-agent -- <glob|file> [...]\n" + " Or set GIT_BASE_SHA for CI mode.");
32+
process.exit(1);
33+
}
34+
35+
async function main(): Promise<void> {
36+
const allFiles = await resolveFiles();
37+
const mdFiles = allFiles.filter(f => f.endsWith(".md") && fs.existsSync(f));
38+
39+
if (mdFiles.length === 0) {
40+
console.log("No .md files to scan, skipping.");
41+
process.exit(0);
42+
}
43+
44+
printChangedFiles(mdFiles);
45+
46+
const results: ScanResult[] = [];
47+
for (const file of mdFiles) {
48+
printScanHeader(file);
49+
const result = scanFile(file);
50+
results.push(result);
51+
if (result.unauthorized) {
52+
printSummary(results);
53+
break;
54+
}
55+
}
56+
57+
printSummary(results);
58+
}
59+
60+
main();
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { execSync } from "child_process";
2+
3+
export function getChangedFiles(baseSha: string, paths: string[]): string[] {
4+
const pathArgs = paths.map(p => `-- ${p}`).join(" ");
5+
const cmd = `git diff --name-only --diff-filter=AM ${baseSha} ${pathArgs}`;
6+
const output = execSync(cmd, { encoding: "utf-8" }).trim();
7+
if (!output) return [];
8+
return output.split("\n").filter(Boolean);
9+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type { ScanResult } from "./scanner.js";
2+
3+
const RED = "\x1b[0;31m";
4+
const GREEN = "\x1b[0;32m";
5+
const YELLOW = "\x1b[1;33m";
6+
const CYAN = "\x1b[0;36m";
7+
const BOLD = "\x1b[1m";
8+
const RESET = "\x1b[0m";
9+
10+
export function printChangedFiles(files: string[]): void {
11+
console.log(`${CYAN}${BOLD}Changed files:${RESET}`);
12+
for (const f of files) console.log(f);
13+
console.log();
14+
}
15+
16+
export function printScanHeader(file: string): void {
17+
console.log(`${CYAN}--- Scanning: ${file} ---${RESET}`);
18+
}
19+
20+
export function printSummary(results: ScanResult[]): void {
21+
const passed = results.filter(r => r.passed);
22+
const failed = results.filter(r => !r.passed);
23+
24+
console.log();
25+
console.log(`${BOLD}========== Scan Summary ==========${RESET}`);
26+
27+
if (passed.length > 0) {
28+
console.log(`${GREEN}${BOLD}PASSED (${passed.length}):${RESET}`);
29+
for (const r of passed) console.log(` ${GREEN}${r.file}${RESET}`);
30+
}
31+
32+
const unauthorized = results.filter(r => r.unauthorized);
33+
34+
if (unauthorized.length > 0) {
35+
console.log(`${YELLOW}${BOLD}UNAUTHORIZED (${unauthorized.length}):${RESET}`);
36+
for (const r of unauthorized) console.log(` ${YELLOW}${r.file}${RESET}`);
37+
console.log(`${YELLOW}${BOLD}SNYK_TOKEN is invalid or missing. Scan results are incomplete.${RESET}`);
38+
process.exit(1);
39+
}
40+
41+
if (failed.length > 0) {
42+
console.log(`${RED}${BOLD}FAILED (${failed.length}):${RESET}`);
43+
for (const r of failed) {
44+
console.log(` ${RED}${r.file}${RESET}`);
45+
console.log(r.output.trimEnd());
46+
console.log();
47+
}
48+
console.log(`${RED}${BOLD}Job failed due to security findings in the files above.${RESET}`);
49+
process.exit(1);
50+
}
51+
52+
console.log(`${GREEN}${BOLD}All scans passed.${RESET}`);
53+
}

0 commit comments

Comments
 (0)