Skip to content

Commit 07b73a6

Browse files
committed
migrate codeql-action into package
1 parent 18be27c commit 07b73a6

File tree

17 files changed

+994
-0
lines changed

17 files changed

+994
-0
lines changed

packages/codeql-action/.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
node_modules/
2+
package-lock.json
3+
.env
4+
pnpm-lock.yaml
5+
package-lock.json
6+
.DS_Store
7+
codeql-config-generated.yml
8+
github_output
9+
.env.test
10+
.yarn/cache
11+
.yarn/install-state.gz

packages/codeql-action/README.md

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# CodeQL Action
2+
3+
Custom CodeQL analysis action with repository-specific configurations and custom query suites.
4+
5+
## Overview
6+
7+
This action provides flexible CodeQL scanning with:
8+
- **Automatic language detection** via GitHub API
9+
- **Repository-specific configurations** in `repo-configs/`
10+
- **Custom query suites** for specialized security analysis
11+
- **Per-language build settings** (version, distribution, build commands)
12+
13+
## Architecture
14+
15+
The action works as part of the security scanning workflow:
16+
17+
1. **Language detector** creates scan matrix from detected languages
18+
2. **Config loader** reads repo-specific config from `repo-configs/<repo-name>.js`
19+
3. **Config generator** merges inputs with repo config and generates CodeQL config
20+
4. **CodeQL** performs analysis and uploads SARIF results
21+
22+
## Inputs
23+
24+
| Input | Required | Description |
25+
|-------|----------|-------------|
26+
| `repo` || Repository name (format: `owner/repo`) |
27+
| `language` || Language to scan (e.g., `javascript-typescript`, `java-kotlin`, `python`) |
28+
| `paths_ignored` || Newline-delimited paths to ignore |
29+
| `rules_excluded` || Newline-delimited CodeQL rule IDs to exclude |
30+
| `build_mode` || Build mode: `none`, `autobuild`, or `manual` |
31+
| `build_command` || Build command for `manual` build mode |
32+
| `version` || Language/runtime version (e.g., `21` for Java, `3.10` for Python) |
33+
| `distribution` || Distribution (e.g., `temurin`, `zulu` for Java) |
34+
35+
## Usage
36+
37+
### Via Reusable Workflow (Recommended)
38+
39+
```yaml
40+
name: Security Scan
41+
on: [push, pull_request]
42+
43+
jobs:
44+
security:
45+
uses: metamask/security-codescanner-monorepo/.github/workflows/security-scan.yml@main
46+
with:
47+
repo: ${{ github.repository }}
48+
permissions:
49+
actions: read
50+
contents: read
51+
security-events: write
52+
```
53+
54+
### Direct Action Usage
55+
56+
```yaml
57+
- name: Run CodeQL Analysis
58+
uses: metamask/security-codescanner-monorepo/packages/codeql-action@main
59+
with:
60+
repo: ${{ github.repository }}
61+
language: javascript-typescript
62+
paths_ignored: |
63+
test/
64+
docs/
65+
rules_excluded: |
66+
js/log-injection
67+
```
68+
69+
## Repository Configuration
70+
71+
### File-Based Config
72+
73+
Create `repo-configs/<repo-name>.js`:
74+
75+
```javascript
76+
const config = {
77+
// Paths to ignore during scan
78+
pathsIgnored: ['test', 'vendor', 'node_modules'],
79+
80+
// Rule IDs to exclude
81+
rulesExcluded: ['js/log-injection', 'js/unsafe-dynamic-method-access'],
82+
83+
// Per-language configuration
84+
languages_config: [
85+
{
86+
language: 'java-kotlin',
87+
build_mode: 'manual',
88+
build_command: './gradlew :coordinator:app:build',
89+
version: '21',
90+
distribution: 'temurin'
91+
},
92+
{
93+
language: 'javascript-typescript',
94+
// Uses default config (no build needed)
95+
},
96+
{
97+
language: 'cpp',
98+
ignore: true // Skip C++ scanning
99+
}
100+
],
101+
102+
// CodeQL query suites
103+
queries: [
104+
{ name: 'Base security queries', uses: './query-suites/base.qls' },
105+
{ name: 'Custom queries', uses: './custom-queries/query-suites/custom-queries.qls' }
106+
]
107+
};
108+
109+
export default config;
110+
```
111+
112+
### Configuration Priority
113+
114+
1. **Workflow input** (highest priority) - overrides everything
115+
2. **Repo config file** - `repo-configs/<repo-name>.js`
116+
3. **Default config** - `repo-configs/default.js`
117+
118+
### Default Configurations
119+
120+
The action includes sensible defaults for common languages:
121+
122+
```javascript
123+
const DEFAULT_CONFIGS = {
124+
'javascript': { language: 'javascript-typescript' },
125+
'typescript': { language: 'javascript-typescript' },
126+
'python': { language: 'python' },
127+
'go': { language: 'go' },
128+
'java': {
129+
language: 'java-kotlin',
130+
build_mode: 'manual',
131+
build_command: './mvnw compile'
132+
},
133+
'cpp': { language: 'cpp' },
134+
'csharp': { language: 'csharp' },
135+
'ruby': { language: 'ruby' }
136+
};
137+
```
138+
139+
## Supported Languages
140+
141+
| GitHub Language | CodeQL Language | Build Required |
142+
|-----------------|-----------------|----------------|
143+
| JavaScript | `javascript-typescript` | No |
144+
| TypeScript | `javascript-typescript` | No |
145+
| Python | `python` | No |
146+
| Java | `java-kotlin` | Yes (defaults to `./mvnw compile`) |
147+
| Kotlin | `java-kotlin` | Yes |
148+
| Go | `go` | No |
149+
| C/C++ | `cpp` | Yes |
150+
| C# | `csharp` | Yes |
151+
| Ruby | `ruby` | No |
152+
153+
## Build Modes
154+
155+
### `none`
156+
No build needed (interpreted languages like JavaScript, Python)
157+
158+
### `autobuild`
159+
CodeQL automatically detects and runs build (works for simple projects)
160+
161+
### `manual`
162+
Specify exact build command:
163+
```javascript
164+
{
165+
language: 'java-kotlin',
166+
build_mode: 'manual',
167+
build_command: './gradlew clean build'
168+
}
169+
```
170+
171+
## Custom Query Suites
172+
173+
Query suites define which CodeQL queries to run:
174+
175+
**Built-in suites:**
176+
- `./query-suites/base.qls` - Standard security queries
177+
- `./query-suites/linea-monorepo.qls` - Project-specific queries
178+
179+
**Custom queries:**
180+
- Checked out from `metamask/CodeQL-Queries` repository
181+
- Available at `./custom-queries/query-suites/custom-queries.qls`
182+
183+
## Troubleshooting
184+
185+
### Config not loading
186+
- Filename must match repo: `owner/repo` → `repo.js`
187+
- Must use ESM: `export default config`
188+
- Check logs: `[config-loader] Loading config for repository: ...`
189+
190+
### Build failures
191+
- Verify `build_command` works locally
192+
- Check Java version matches `version` input
193+
- Review build step logs in Actions
194+
195+
### Language not detected
196+
- Check GitHub language stats (repo → Insights → Languages)
197+
- Add language manually via `languages_config` in repo config
198+
- Verify language mapping in `language-detector/src/job-configurator.js`
199+
200+
### SARIF upload errors
201+
- Ensure workflow has `security-events: write` permission
202+
- Check SARIF file is generated in `${{ steps.codeql-analysis.outputs.sarif-output }}`
203+
- Review CodeQL analysis logs
204+
205+
## Security
206+
207+
See [SECURITY.md](../../SECURITY.md) for:
208+
- Threat model and security boundaries
209+
- Input validation approach
210+
- Token permissions model
211+
212+
## Development
213+
214+
### Testing Config Changes
215+
216+
```bash
217+
# Run config generator locally
218+
cd packages/codeql-action
219+
REPO=owner/repo LANGUAGE=javascript node scripts/generate-config.js
220+
221+
# Validate generated config
222+
cat codeql-config-generated.yml
223+
```
224+
225+
### Adding New Language Support
226+
227+
1. Add to `LANGUAGE_MAPPING` in `language-detector/src/job-configurator.js`
228+
2. Add default config in `language-detector/src/job-configurator.js` → `DEFAULT_CONFIGS`
229+
3. Update this README's supported languages table
230+
231+
## License
232+
233+
ISC
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { loadRepoConfig, getDefaultConfig } from '../src/config-loader.js';
2+
import fs from 'fs';
3+
import path from 'path';
4+
import { fileURLToPath } from 'url';
5+
6+
const __filename = fileURLToPath(import.meta.url);
7+
const __dirname = path.dirname(__filename);
8+
9+
describe('config-loader', () => {
10+
describe('getDefaultConfig', () => {
11+
test('returns default configuration', () => {
12+
const config = getDefaultConfig();
13+
14+
expect(config).toHaveProperty('pathsIgnored');
15+
expect(config).toHaveProperty('rulesExcluded');
16+
expect(config).toHaveProperty('languages_config');
17+
expect(config).toHaveProperty('queries');
18+
19+
expect(Array.isArray(config.pathsIgnored)).toBe(true);
20+
expect(Array.isArray(config.rulesExcluded)).toBe(true);
21+
expect(Array.isArray(config.queries)).toBe(true);
22+
});
23+
24+
test('includes expected default values', () => {
25+
const config = getDefaultConfig();
26+
27+
expect(config.pathsIgnored).toContain('test');
28+
expect(config.rulesExcluded).toContain('js/log-injection');
29+
expect(config.queries.length).toBeGreaterThan(0);
30+
});
31+
});
32+
33+
describe('loadRepoConfig', () => {
34+
test('loads existing repo config', async () => {
35+
// Test with the existing lll.js config
36+
const configDir = path.join(__dirname, '..', 'repo-configs');
37+
const config = await loadRepoConfig('owner/lll', configDir);
38+
39+
expect(config).toBeDefined();
40+
expect(config).toHaveProperty('pathsIgnored');
41+
expect(config).toHaveProperty('queries');
42+
});
43+
44+
test('falls back to default.js when repo config not found', async () => {
45+
const configDir = path.join(__dirname, '..', 'repo-configs');
46+
const config = await loadRepoConfig('owner/nonexistent-repo', configDir);
47+
48+
// Should return default config
49+
expect(config).toBeDefined();
50+
expect(config.pathsIgnored).toContain('test');
51+
});
52+
53+
test('returns default config when config directory does not exist', async () => {
54+
const config = await loadRepoConfig('owner/repo', '/nonexistent/path');
55+
56+
expect(config).toBeDefined();
57+
expect(config).toHaveProperty('pathsIgnored');
58+
expect(config).toHaveProperty('rulesExcluded');
59+
});
60+
61+
test('handles malformed config gracefully', async () => {
62+
// Create temp directory with config that throws an error
63+
const tempDir = path.join(__dirname, 'temp-configs');
64+
fs.mkdirSync(tempDir, { recursive: true });
65+
66+
try {
67+
// Write a config that will cause an error when evaluated (but is valid JS)
68+
const badConfigPath = path.join(tempDir, 'errorconfig.js');
69+
fs.writeFileSync(badConfigPath, 'throw new Error("Config error"); export default {};');
70+
71+
const config = await loadRepoConfig('owner/errorconfig', tempDir);
72+
73+
// Should fall back to default config
74+
expect(config).toBeDefined();
75+
expect(config.pathsIgnored).toContain('test');
76+
} finally {
77+
// Cleanup - ensure it runs even if test fails
78+
try {
79+
fs.rmSync(tempDir, { recursive: true, force: true });
80+
} catch (e) {
81+
// Ignore cleanup errors
82+
}
83+
}
84+
});
85+
86+
test('extracts repo name from owner/repo format', async () => {
87+
const configDir = path.join(__dirname, '..', 'repo-configs');
88+
89+
// Should look for lll.js (not owner/lll.js)
90+
const config = await loadRepoConfig('metamask/lll', configDir);
91+
92+
expect(config).toBeDefined();
93+
// If lll.js exists, it should load it; otherwise default
94+
});
95+
});
96+
});

0 commit comments

Comments
 (0)