@authtrack/secura 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +229 -157
- package/bin/secura-mcp.mjs +412 -0
- package/package.json +54 -46
- package/src/config.mjs +1 -1
package/README.md
CHANGED
|
@@ -1,157 +1,229 @@
|
|
|
1
|
-
# @authtrack/secura
|
|
2
|
-
|
|
3
|
-
**AuthTrack static analysis (SAST) from the command line.**
|
|
4
|
-
|
|
5
|
-
`secura` points five independent engines — [Semgrep], [Bearer], [OSV-Scanner],
|
|
6
|
-
[Gitleaks] and [CodeQL] — at a repository, merges and deduplicates their
|
|
7
|
-
findings, retrieves similar known-vulnerable patterns, then has an LLM confirm
|
|
8
|
-
or rule out each finding and write a patch for the confirmed ones. It streams
|
|
9
|
-
the whole run live in your terminal and prints a report at the end.
|
|
10
|
-
|
|
11
|
-
The scanning runs on the **AuthTrack backend**; this CLI is a thin, zero-dependency
|
|
12
|
-
client that talks to it. You don't need Python, and you don't need the scanners
|
|
13
|
-
installed locally.
|
|
14
|
-
|
|
15
|
-
[Semgrep]: https://semgrep.dev
|
|
16
|
-
[Bearer]: https://www.bearer.com
|
|
17
|
-
[OSV-Scanner]: https://google.github.io/osv-scanner/
|
|
18
|
-
[Gitleaks]: https://github.com/gitleaks/gitleaks
|
|
19
|
-
[CodeQL]: https://codeql.github.com
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## Install
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
npm install -g @authtrack/secura
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Or run it once, without installing:
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
npx @authtrack/secura scan .
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Requires **Node ≥ 18**. No build step, no native modules.
|
|
36
|
-
|
|
37
|
-
## Quick start
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
# Scan the current directory
|
|
41
|
-
secura scan .
|
|
42
|
-
|
|
43
|
-
# Scan a public GitHub repo (the backend clones it)
|
|
44
|
-
secura scan owner/repo
|
|
45
|
-
secura scan https://github.com/owner/repo
|
|
46
|
-
|
|
47
|
-
# Fast pass — skip CodeQL (its database build dominates the run)
|
|
48
|
-
secura scan . --fast
|
|
49
|
-
|
|
50
|
-
# CI gate: exit non-zero if anything HIGH or worse is confirmed
|
|
51
|
-
secura scan . --fail-on high
|
|
52
|
-
|
|
53
|
-
# Save the full machine-readable report
|
|
54
|
-
secura scan . --json report.json
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
## Targets
|
|
58
|
-
|
|
59
|
-
| Target | What happens |
|
|
60
|
-
| --- | --- |
|
|
61
|
-
| `.` or `./path/to/repo` | A local folder. Sent to the backend to scan (see [Backend](#backend)). |
|
|
62
|
-
| `owner/repo` | Public GitHub shorthand. The backend shallow-clones it, scans, then deletes it. |
|
|
63
|
-
| `https://github.com/owner/repo`, `git@…`, `ssh://…` | Any public Git URL. Cloned by the backend. |
|
|
64
|
-
|
|
65
|
-
For a local path, how the code reaches the backend depends on where the backend runs:
|
|
66
|
-
|
|
67
|
-
- **Local backend** (the default, `http://localhost:8000`): the absolute path is
|
|
68
|
-
sent as-is and scanned in place — nothing is copied.
|
|
69
|
-
- **Remote backend** (a `--api` that isn't localhost), or **`--upload`**: the
|
|
70
|
-
directory is packaged into a `tar.gz` (excluding `node_modules`, `.git`, build
|
|
71
|
-
output, lockfiles, minified assets, …) and uploaded. Requires `tar` on your
|
|
72
|
-
PATH (bundled with Windows 10 1803+, macOS and Linux). If `tar` is missing,
|
|
73
|
-
scan a Git URL instead.
|
|
74
|
-
|
|
75
|
-
## Options
|
|
76
|
-
|
|
77
|
-
```
|
|
78
|
-
-f, --fast Skip CodeQL (its DB build dominates the run).
|
|
79
|
-
-l, --language <lang> CodeQL language (javascript, python, java, go, ruby,
|
|
80
|
-
csharp, cpp). Auto-detected when omitted.
|
|
81
|
-
-t, --max-triage <n> Cap findings sent to LLM triage (default 25).
|
|
82
|
-
--fail-on <sev> Exit 1 if a confirmed finding is >= this severity
|
|
83
|
-
(critical|high|medium|low). For CI gates.
|
|
84
|
-
-o, --json <path> Write the full report as JSON to <path>.
|
|
85
|
-
--max-findings <n> How many confirmed findings to print (default 20).
|
|
86
|
-
--show-fixes Print the generated fix diffs in full.
|
|
87
|
-
-q, --quiet Suppress the live view; print only the report.
|
|
88
|
-
--api <url> Backend base URL (default $SECURA_API or
|
|
89
|
-
http://localhost:8000/api/v1).
|
|
90
|
-
--token <token> Bearer token, if the backend requires one
|
|
91
|
-
(default $SECURA_TOKEN).
|
|
92
|
-
--upload Package and upload the local dir even for a local
|
|
93
|
-
backend (automatic for a remote --api).
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### Exit codes
|
|
97
|
-
|
|
98
|
-
| Code | Meaning |
|
|
99
|
-
| --- | --- |
|
|
100
|
-
| `0` | Completed; no `--fail-on` breach. |
|
|
101
|
-
| `1` | `--fail-on` gate breached (a confirmed finding at or above the threshold). |
|
|
102
|
-
| `2` | Bad target, or the scan failed. |
|
|
103
|
-
| `130` | Interrupted (Ctrl-C). |
|
|
104
|
-
|
|
105
|
-
## Backend
|
|
106
|
-
|
|
107
|
-
`secura` needs a reachable AuthTrack backend. Point it at one with `--api` or the
|
|
108
|
-
`SECURA_API` environment variable (matching the web app's `NEXT_PUBLIC_API_URL`):
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
export SECURA_API="https://scans.example.com/api/v1"
|
|
112
|
-
secura scan owner/repo
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
The default is `http://localhost:8000/api/v1`. The scan endpoints are unauthenticated;
|
|
116
|
-
`--token` / `SECURA_TOKEN` is sent as a bearer header only if provided, so it keeps
|
|
117
|
-
working if auth is added later.
|
|
118
|
-
|
|
119
|
-
## CI example
|
|
120
|
-
|
|
121
|
-
```yaml
|
|
122
|
-
# GitHub Actions — fail the build on a confirmed high+ finding
|
|
123
|
-
- name: SAST
|
|
124
|
-
run: npx @authtrack/secura scan . --fast --fail-on high --json sast.json
|
|
125
|
-
env:
|
|
126
|
-
SECURA_API: ${{ secrets.SECURA_API }}
|
|
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
|
-
|
|
1
|
+
# @authtrack/secura
|
|
2
|
+
|
|
3
|
+
**AuthTrack static analysis (SAST) from the command line.**
|
|
4
|
+
|
|
5
|
+
`secura` points five independent engines — [Semgrep], [Bearer], [OSV-Scanner],
|
|
6
|
+
[Gitleaks] and [CodeQL] — at a repository, merges and deduplicates their
|
|
7
|
+
findings, retrieves similar known-vulnerable patterns, then has an LLM confirm
|
|
8
|
+
or rule out each finding and write a patch for the confirmed ones. It streams
|
|
9
|
+
the whole run live in your terminal and prints a report at the end.
|
|
10
|
+
|
|
11
|
+
The scanning runs on the **AuthTrack backend**; this CLI is a thin, zero-dependency
|
|
12
|
+
client that talks to it. You don't need Python, and you don't need the scanners
|
|
13
|
+
installed locally.
|
|
14
|
+
|
|
15
|
+
[Semgrep]: https://semgrep.dev
|
|
16
|
+
[Bearer]: https://www.bearer.com
|
|
17
|
+
[OSV-Scanner]: https://google.github.io/osv-scanner/
|
|
18
|
+
[Gitleaks]: https://github.com/gitleaks/gitleaks
|
|
19
|
+
[CodeQL]: https://codeql.github.com
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install -g @authtrack/secura
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or run it once, without installing:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx @authtrack/secura scan .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requires **Node ≥ 18**. No build step, no native modules.
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Scan the current directory
|
|
41
|
+
secura scan .
|
|
42
|
+
|
|
43
|
+
# Scan a public GitHub repo (the backend clones it)
|
|
44
|
+
secura scan owner/repo
|
|
45
|
+
secura scan https://github.com/owner/repo
|
|
46
|
+
|
|
47
|
+
# Fast pass — skip CodeQL (its database build dominates the run)
|
|
48
|
+
secura scan . --fast
|
|
49
|
+
|
|
50
|
+
# CI gate: exit non-zero if anything HIGH or worse is confirmed
|
|
51
|
+
secura scan . --fail-on high
|
|
52
|
+
|
|
53
|
+
# Save the full machine-readable report
|
|
54
|
+
secura scan . --json report.json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Targets
|
|
58
|
+
|
|
59
|
+
| Target | What happens |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| `.` or `./path/to/repo` | A local folder. Sent to the backend to scan (see [Backend](#backend)). |
|
|
62
|
+
| `owner/repo` | Public GitHub shorthand. The backend shallow-clones it, scans, then deletes it. |
|
|
63
|
+
| `https://github.com/owner/repo`, `git@…`, `ssh://…` | Any public Git URL. Cloned by the backend. |
|
|
64
|
+
|
|
65
|
+
For a local path, how the code reaches the backend depends on where the backend runs:
|
|
66
|
+
|
|
67
|
+
- **Local backend** (the default, `http://localhost:8000`): the absolute path is
|
|
68
|
+
sent as-is and scanned in place — nothing is copied.
|
|
69
|
+
- **Remote backend** (a `--api` that isn't localhost), or **`--upload`**: the
|
|
70
|
+
directory is packaged into a `tar.gz` (excluding `node_modules`, `.git`, build
|
|
71
|
+
output, lockfiles, minified assets, …) and uploaded. Requires `tar` on your
|
|
72
|
+
PATH (bundled with Windows 10 1803+, macOS and Linux). If `tar` is missing,
|
|
73
|
+
scan a Git URL instead.
|
|
74
|
+
|
|
75
|
+
## Options
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
-f, --fast Skip CodeQL (its DB build dominates the run).
|
|
79
|
+
-l, --language <lang> CodeQL language (javascript, python, java, go, ruby,
|
|
80
|
+
csharp, cpp). Auto-detected when omitted.
|
|
81
|
+
-t, --max-triage <n> Cap findings sent to LLM triage (default 25).
|
|
82
|
+
--fail-on <sev> Exit 1 if a confirmed finding is >= this severity
|
|
83
|
+
(critical|high|medium|low). For CI gates.
|
|
84
|
+
-o, --json <path> Write the full report as JSON to <path>.
|
|
85
|
+
--max-findings <n> How many confirmed findings to print (default 20).
|
|
86
|
+
--show-fixes Print the generated fix diffs in full.
|
|
87
|
+
-q, --quiet Suppress the live view; print only the report.
|
|
88
|
+
--api <url> Backend base URL (default $SECURA_API or
|
|
89
|
+
http://localhost:8000/api/v1).
|
|
90
|
+
--token <token> Bearer token, if the backend requires one
|
|
91
|
+
(default $SECURA_TOKEN).
|
|
92
|
+
--upload Package and upload the local dir even for a local
|
|
93
|
+
backend (automatic for a remote --api).
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Exit codes
|
|
97
|
+
|
|
98
|
+
| Code | Meaning |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| `0` | Completed; no `--fail-on` breach. |
|
|
101
|
+
| `1` | `--fail-on` gate breached (a confirmed finding at or above the threshold). |
|
|
102
|
+
| `2` | Bad target, or the scan failed. |
|
|
103
|
+
| `130` | Interrupted (Ctrl-C). |
|
|
104
|
+
|
|
105
|
+
## Backend
|
|
106
|
+
|
|
107
|
+
`secura` needs a reachable AuthTrack backend. Point it at one with `--api` or the
|
|
108
|
+
`SECURA_API` environment variable (matching the web app's `NEXT_PUBLIC_API_URL`):
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
export SECURA_API="https://scans.example.com/api/v1"
|
|
112
|
+
secura scan owner/repo
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The default is `http://localhost:8000/api/v1`. The scan endpoints are unauthenticated;
|
|
116
|
+
`--token` / `SECURA_TOKEN` is sent as a bearer header only if provided, so it keeps
|
|
117
|
+
working if auth is added later.
|
|
118
|
+
|
|
119
|
+
## CI example
|
|
120
|
+
|
|
121
|
+
```yaml
|
|
122
|
+
# GitHub Actions — fail the build on a confirmed high+ finding
|
|
123
|
+
- name: SAST
|
|
124
|
+
run: npx @authtrack/secura scan . --fast --fail-on high --json sast.json
|
|
125
|
+
env:
|
|
126
|
+
SECURA_API: ${{ secrets.SECURA_API }}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## MCP Server (`secura-mcp`)
|
|
132
|
+
|
|
133
|
+
After `npm install -g @authtrack/secura`, a second command `secura-mcp` is also installed.
|
|
134
|
+
**You never run it yourself** — your AI editor runs it automatically as a background subprocess
|
|
135
|
+
and exposes its tools to the AI assistant.
|
|
136
|
+
|
|
137
|
+
### Tools exposed
|
|
138
|
+
|
|
139
|
+
| Tool | What it does |
|
|
140
|
+
|------|-------------|
|
|
141
|
+
| `run_static_scan` | SAST — Semgrep, Bearer, OSV-Scanner, Gitleaks, CodeQL + AI triage |
|
|
142
|
+
| `run_recon` | Surface mapping — crawl & classify endpoints |
|
|
143
|
+
| `run_header_audit` | OWASP security-header & cookie audit |
|
|
144
|
+
|
|
145
|
+
### Claude Code setup
|
|
146
|
+
|
|
147
|
+
Create or edit `~/.claude/claude_desktop_config.json` (or the path shown in Claude's settings):
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"mcpServers": {
|
|
152
|
+
"secura": {
|
|
153
|
+
"command": "secura-mcp",
|
|
154
|
+
"env": {
|
|
155
|
+
"SECURA_API": "http://localhost:8000/api/v1",
|
|
156
|
+
"SECURA_TOKEN": "<your-jwt-if-needed>"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Cursor setup
|
|
164
|
+
|
|
165
|
+
Open **Cursor → Settings → Features → MCP** and add:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"secura": {
|
|
170
|
+
"command": "secura-mcp",
|
|
171
|
+
"env": {
|
|
172
|
+
"SECURA_API": "http://localhost:8000/api/v1",
|
|
173
|
+
"SECURA_TOKEN": "<your-jwt-if-needed>"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Usage (after setup)
|
|
180
|
+
|
|
181
|
+
Just talk to your AI assistant as normal — it will automatically call the tools when relevant:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
"Check this repo for vulnerabilities"
|
|
185
|
+
→ AI calls run_static_scan({ target_path: "." })
|
|
186
|
+
|
|
187
|
+
"Map the attack surface of https://example.com"
|
|
188
|
+
→ AI calls run_recon({ url: "https://example.com" })
|
|
189
|
+
|
|
190
|
+
"Audit the security headers on my staging server"
|
|
191
|
+
→ AI calls run_header_audit({ url: "https://staging.example.com" })
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Environment variables
|
|
195
|
+
|
|
196
|
+
| Variable | Default | Description |
|
|
197
|
+
|----------|---------|-------------|
|
|
198
|
+
| `SECURA_API` | `https://securaai-major-project.onrender.com/api/v1` | Backend base URL |
|
|
199
|
+
| `SECURA_TOKEN` | _(empty)_ | Bearer token for authenticated backends |
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
This package publishes from the `cli/` directory of the AuthTrack repo. It is
|
|
206
|
+
plain ESM with no build step, so publishing is just:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
cd cli
|
|
210
|
+
npm login # once per machine
|
|
211
|
+
npm version patch # 0.1.0 -> 0.1.1 (bumps package.json + git tag)
|
|
212
|
+
npm publish --access public # scoped packages need --access public on first publish
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Notes:
|
|
216
|
+
|
|
217
|
+
- **Scope.** The name `@authtrack/secura` requires the `@SecuraAI` org to exist
|
|
218
|
+
on npm and your account to be a member. Create it at
|
|
219
|
+
<https://www.npmjs.com/org/create>, or rename the package to an unscoped name
|
|
220
|
+
you own (e.g. `securai-secura`) in `package.json` — the `bin` stays `secura`
|
|
221
|
+
either way, so `secura scan …` is unchanged for users.
|
|
222
|
+
- **What ships.** Only `bin/`, `src/` and `README.md` (the `files` allowlist).
|
|
223
|
+
Verify with `npm pack --dry-run` before publishing.
|
|
224
|
+
- **Smoke test the tarball.** `npm pack` then
|
|
225
|
+
`npm i -g ./securai-secura-<version>.tgz` and run `secura --help`.
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
MIT
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* secura-mcp — SecuraAI MCP server (stdio transport).
|
|
4
|
+
*
|
|
5
|
+
* Exposes four security-scanning tools to any MCP-capable AI editor
|
|
6
|
+
* (Claude Code, Cursor, Continue, etc.). The editor spawns this process
|
|
7
|
+
* automatically; users never run it themselves.
|
|
8
|
+
*
|
|
9
|
+
* Tools exposed:
|
|
10
|
+
* run_static_scan — SAST (Semgrep, Bearer, OSV-Scanner, Gitleaks, CodeQL)
|
|
11
|
+
* run_recon — Surface mapping / endpoint discovery
|
|
12
|
+
* run_header_audit — Security-header & cookie audit
|
|
13
|
+
* run_injection_scan — SQL / command / XSS injection testing
|
|
14
|
+
*
|
|
15
|
+
* Configuration (environment variables, all optional):
|
|
16
|
+
* SECURA_API — Backend base URL (default: https://securaai-major-project.onrender.com/api/v1)
|
|
17
|
+
* SECURA_TOKEN — Bearer token for authenticated backends
|
|
18
|
+
*
|
|
19
|
+
* Usage in Claude Code / Cursor mcp config:
|
|
20
|
+
* {
|
|
21
|
+
* "mcpServers": {
|
|
22
|
+
* "secura": {
|
|
23
|
+
* "command": "secura-mcp",
|
|
24
|
+
* "env": {
|
|
25
|
+
* "SECURA_API": "http://localhost:8000/api/v1",
|
|
26
|
+
* "SECURA_TOKEN": "<your-jwt>"
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
34
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
35
|
+
import {
|
|
36
|
+
CallToolRequestSchema,
|
|
37
|
+
ListToolsRequestSchema,
|
|
38
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
39
|
+
|
|
40
|
+
// ── Config ───────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
const DEFAULT_API = "https://securaai-major-project.onrender.com/api/v1";
|
|
43
|
+
|
|
44
|
+
function resolveConfig() {
|
|
45
|
+
const api = (process.env.SECURA_API || DEFAULT_API).replace(/\/+$/, "");
|
|
46
|
+
const token = process.env.SECURA_TOKEN || "";
|
|
47
|
+
return { api, token };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function authHeaders(token, extra = {}) {
|
|
51
|
+
const h = { ...extra };
|
|
52
|
+
if (token) h.Authorization = `Bearer ${token}`;
|
|
53
|
+
return h;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── SSE streaming helpers ────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
async function* readSse(res) {
|
|
59
|
+
const decoder = new TextDecoder();
|
|
60
|
+
const reader = res.body.getReader();
|
|
61
|
+
let buffer = "";
|
|
62
|
+
|
|
63
|
+
const emit = function* (frame) {
|
|
64
|
+
for (const line of frame.split("\n")) {
|
|
65
|
+
if (!line.startsWith("data:")) continue;
|
|
66
|
+
const json = line.slice(line.indexOf(":") + 1).trim();
|
|
67
|
+
if (!json) continue;
|
|
68
|
+
try { yield JSON.parse(json); } catch { /* skip malformed */ }
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
while (true) {
|
|
73
|
+
const { done, value } = await reader.read();
|
|
74
|
+
if (done) break;
|
|
75
|
+
buffer += decoder.decode(value, { stream: true });
|
|
76
|
+
const frames = buffer.split("\n\n");
|
|
77
|
+
buffer = frames.pop() || "";
|
|
78
|
+
for (const f of frames) yield* emit(f);
|
|
79
|
+
}
|
|
80
|
+
const tail = buffer.trim();
|
|
81
|
+
if (tail) yield* emit(tail);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Drain an SSE stream and collect the accumulated graph state.
|
|
86
|
+
* Returns { finalState, scanId, error }.
|
|
87
|
+
*/
|
|
88
|
+
async function drainStream(url, token, headers, body) {
|
|
89
|
+
let res;
|
|
90
|
+
try {
|
|
91
|
+
res = await fetch(url, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: authHeaders(token, headers),
|
|
94
|
+
body,
|
|
95
|
+
});
|
|
96
|
+
} catch (err) {
|
|
97
|
+
throw new Error(`Cannot reach SecuraAI backend at ${url}: ${err.message}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
const detail = await res.text().catch(() => "");
|
|
102
|
+
throw new Error(`Backend error ${res.status}: ${detail.slice(0, 400)}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const accumulated = {};
|
|
106
|
+
let scanId = null;
|
|
107
|
+
let lastError = null;
|
|
108
|
+
|
|
109
|
+
for await (const evt of readSse(res)) {
|
|
110
|
+
if (evt.event === "node_update" && evt.state) {
|
|
111
|
+
Object.assign(accumulated, evt.state);
|
|
112
|
+
}
|
|
113
|
+
if (evt.event === "complete") {
|
|
114
|
+
scanId = evt.scan_id ?? null;
|
|
115
|
+
}
|
|
116
|
+
if (evt.event === "error") {
|
|
117
|
+
lastError = evt.message;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { finalState: accumulated, scanId, error: lastError };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Tool implementations ─────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
/** SAST — static analysis scan. */
|
|
127
|
+
async function runStaticScan(args) {
|
|
128
|
+
const { api, token } = resolveConfig();
|
|
129
|
+
const body = JSON.stringify({
|
|
130
|
+
target_path: args.target_path,
|
|
131
|
+
include_codeql: args.include_codeql ?? true,
|
|
132
|
+
codeql_language: args.codeql_language ?? "",
|
|
133
|
+
max_triage: args.max_triage ?? 25,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const { finalState, scanId, error } = await drainStream(
|
|
137
|
+
`${api}/scan/stream/static`, token,
|
|
138
|
+
{ "Content-Type": "application/json" }, body,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (error) throw new Error(error);
|
|
142
|
+
|
|
143
|
+
const report = finalState.static_report ?? {};
|
|
144
|
+
return formatStaticReport(report, scanId);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Recon — surface mapping / endpoint discovery. */
|
|
148
|
+
async function runRecon(args) {
|
|
149
|
+
const { api, token } = resolveConfig();
|
|
150
|
+
const body = JSON.stringify({
|
|
151
|
+
url: args.url,
|
|
152
|
+
max_depth: args.max_depth ?? 2,
|
|
153
|
+
max_pages: args.max_pages ?? 30,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const { finalState, scanId, error } = await drainStream(
|
|
157
|
+
`${api}/scan/stream/recon`, token,
|
|
158
|
+
{ "Content-Type": "application/json" }, body,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
if (error) throw new Error(error);
|
|
162
|
+
|
|
163
|
+
const report = finalState.surface_report ?? {};
|
|
164
|
+
const endpoints = finalState.classified_endpoints ?? report.endpoints ?? [];
|
|
165
|
+
return formatReconReport(report, endpoints, scanId);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Header audit — security-header & cookie checks. */
|
|
169
|
+
async function runHeaderAudit(args) {
|
|
170
|
+
const { api, token } = resolveConfig();
|
|
171
|
+
const body = JSON.stringify({
|
|
172
|
+
url: args.url,
|
|
173
|
+
max_depth: args.max_depth ?? 2,
|
|
174
|
+
max_pages: args.max_pages ?? 30,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const { finalState, scanId, error } = await drainStream(
|
|
178
|
+
`${api}/scan/stream/full`, token,
|
|
179
|
+
{ "Content-Type": "application/json" }, body,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (error) throw new Error(error);
|
|
183
|
+
|
|
184
|
+
const auditReport = finalState.audit_report ?? {};
|
|
185
|
+
const surfaceReport = finalState.surface_report ?? {};
|
|
186
|
+
return formatHeaderReport(auditReport, surfaceReport, scanId);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
// ── Formatters ───────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
function formatStaticReport(report, scanId) {
|
|
194
|
+
const lines = [
|
|
195
|
+
"## SecuraAI Static Analysis Report",
|
|
196
|
+
"",
|
|
197
|
+
`**Target:** ${report.target ?? "unknown"}`,
|
|
198
|
+
`**Scan ID:** ${scanId ?? "n/a"}`,
|
|
199
|
+
`**Summary:** ${report.summary ?? "No summary available."}`,
|
|
200
|
+
"",
|
|
201
|
+
`| Metric | Count |`,
|
|
202
|
+
`|--------|-------|`,
|
|
203
|
+
`| Unique findings (after merge) | ${report.total_after_merge ?? 0} |`,
|
|
204
|
+
`| Confirmed vulnerabilities | ${report.confirmed_count ?? 0} |`,
|
|
205
|
+
`| Ruled out (false positives) | ${report.ruled_out_count ?? 0} |`,
|
|
206
|
+
`| Fixes generated | ${report.fixes_count ?? 0} |`,
|
|
207
|
+
`| With CodeQL taint path | ${report.with_taint_path_count ?? 0} |`,
|
|
208
|
+
"",
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
const confirmed = report.confirmed_findings ?? [];
|
|
212
|
+
if (confirmed.length > 0) {
|
|
213
|
+
lines.push("### Confirmed Vulnerabilities", "");
|
|
214
|
+
confirmed.slice(0, 20).forEach((f, i) => {
|
|
215
|
+
const sev = (f.final_severity || f.severity || "info").toUpperCase();
|
|
216
|
+
const evidence = f.taint_path ? "taint path"
|
|
217
|
+
: f.multi_tool_confirmed ? "multi-tool"
|
|
218
|
+
: "single tool";
|
|
219
|
+
lines.push(
|
|
220
|
+
`**${i + 1}. [${sev}] ${f.category ?? f.rule_id}**`,
|
|
221
|
+
`- File: \`${f.file}:${f.line}\``,
|
|
222
|
+
`- Rule: \`${f.rule_id}\``,
|
|
223
|
+
`- Evidence: ${evidence}`,
|
|
224
|
+
f.triage_reason ? `- Reason: ${f.triage_reason}` : null,
|
|
225
|
+
"",
|
|
226
|
+
).filter(Boolean);
|
|
227
|
+
});
|
|
228
|
+
if (confirmed.length > 20) lines.push(`_…and ${confirmed.length - 20} more_`, "");
|
|
229
|
+
} else {
|
|
230
|
+
lines.push("✅ No confirmed vulnerabilities found.", "");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { content: [{ type: "text", text: lines.filter(l => l !== null).join("\n") }] };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function formatReconReport(report, endpoints, scanId) {
|
|
237
|
+
const lines = [
|
|
238
|
+
"## SecuraAI Recon Report",
|
|
239
|
+
"",
|
|
240
|
+
`**Target:** ${report.target_url ?? "unknown"}`,
|
|
241
|
+
`**Scan ID:** ${scanId ?? "n/a"}`,
|
|
242
|
+
`**Total Endpoints:** ${report.total_endpoints ?? endpoints.length}`,
|
|
243
|
+
`**Scanned At:** ${report.scan_timestamp ?? new Date().toISOString()}`,
|
|
244
|
+
"",
|
|
245
|
+
report.summary ? `### Summary\n\n${report.summary}\n` : null,
|
|
246
|
+
"### Endpoints Discovered",
|
|
247
|
+
"",
|
|
248
|
+
`| URL | Method | Type | Status |`,
|
|
249
|
+
`|-----|--------|------|--------|`,
|
|
250
|
+
].filter(Boolean);
|
|
251
|
+
|
|
252
|
+
endpoints.slice(0, 50).forEach(ep => {
|
|
253
|
+
lines.push(
|
|
254
|
+
`| \`${ep.url}\` | ${ep.method ?? "GET"} | ${ep.endpoint_type ?? "unknown"} | ${ep.status_code ?? "-"} |`
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if (endpoints.length > 50) lines.push(`\n_…and ${endpoints.length - 50} more endpoints_`);
|
|
259
|
+
|
|
260
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function formatHeaderReport(audit, surface, scanId) {
|
|
264
|
+
const lines = [
|
|
265
|
+
"## SecuraAI Header Audit Report",
|
|
266
|
+
"",
|
|
267
|
+
`**Target:** ${audit.target_url ?? surface.target_url ?? "unknown"}`,
|
|
268
|
+
`**Scan ID:** ${scanId ?? "n/a"}`,
|
|
269
|
+
`**Total Findings:** ${audit.total_findings ?? 0}`,
|
|
270
|
+
"",
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
const findings = audit.findings ?? [];
|
|
274
|
+
if (findings.length > 0) {
|
|
275
|
+
lines.push("### Findings", "");
|
|
276
|
+
findings.slice(0, 30).forEach((f, i) => {
|
|
277
|
+
const sev = (f.severity || "info").toUpperCase();
|
|
278
|
+
lines.push(
|
|
279
|
+
`**${i + 1}. [${sev}] ${f.header_name ?? f.category}**`,
|
|
280
|
+
`- URL: \`${f.url}\``,
|
|
281
|
+
`- Issue: ${f.issue ?? f.expected ?? ""}`,
|
|
282
|
+
f.recommendation ? `- Fix: ${f.recommendation}` : null,
|
|
283
|
+
"",
|
|
284
|
+
).filter(Boolean);
|
|
285
|
+
});
|
|
286
|
+
} else {
|
|
287
|
+
lines.push("✅ No header misconfigurations found.", "");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { content: [{ type: "text", text: lines.filter(l => l !== null).join("\n") }] };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
// ── Tool definitions (shown to the AI) ───────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
const TOOLS = [
|
|
298
|
+
{
|
|
299
|
+
name: "run_static_scan",
|
|
300
|
+
description: "Run SAST static analysis on a local code repository using Semgrep, Bearer, OSV-Scanner, Gitleaks and CodeQL. Returns confirmed vulnerabilities, false positives, and generated fixes.",
|
|
301
|
+
inputSchema: {
|
|
302
|
+
type: "object",
|
|
303
|
+
properties: {
|
|
304
|
+
target_path: {
|
|
305
|
+
type: "string",
|
|
306
|
+
description: "Absolute or relative path to the local repository/folder to scan. Can also be a GitHub URL (https://github.com/owner/repo) or 'owner/repo' shorthand.",
|
|
307
|
+
},
|
|
308
|
+
include_codeql: {
|
|
309
|
+
type: "boolean",
|
|
310
|
+
description: "Include CodeQL taint analysis (slower but finds deeper data-flow issues). Default: true.",
|
|
311
|
+
default: true,
|
|
312
|
+
},
|
|
313
|
+
codeql_language: {
|
|
314
|
+
type: "string",
|
|
315
|
+
description: "CodeQL extractor language (javascript, python, java, go, ruby, csharp, cpp). Leave empty to auto-detect.",
|
|
316
|
+
default: "",
|
|
317
|
+
},
|
|
318
|
+
max_triage: {
|
|
319
|
+
type: "number",
|
|
320
|
+
description: "Maximum number of merged findings to AI-triage. Default: 25.",
|
|
321
|
+
default: 25,
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
required: ["target_path"],
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
name: "run_recon",
|
|
329
|
+
description: "Crawl a web application and map its attack surface — discovering endpoints, classifying them (API, auth, static, etc.), and producing a surface report with an LLM-written summary.",
|
|
330
|
+
inputSchema: {
|
|
331
|
+
type: "object",
|
|
332
|
+
properties: {
|
|
333
|
+
url: {
|
|
334
|
+
type: "string",
|
|
335
|
+
description: "The target URL to crawl (e.g. https://example.com).",
|
|
336
|
+
},
|
|
337
|
+
max_depth: {
|
|
338
|
+
type: "number",
|
|
339
|
+
description: "Maximum crawl depth from the seed URL. Default: 2.",
|
|
340
|
+
default: 2,
|
|
341
|
+
},
|
|
342
|
+
max_pages: {
|
|
343
|
+
type: "number",
|
|
344
|
+
description: "Maximum number of pages to visit. Default: 30.",
|
|
345
|
+
default: 30,
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
required: ["url"],
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: "run_header_audit",
|
|
353
|
+
description: "Crawl a web application and audit its HTTP security headers and cookies against OWASP best practices (CSP, HSTS, X-Frame-Options, SameSite cookies, CORS, etc.).",
|
|
354
|
+
inputSchema: {
|
|
355
|
+
type: "object",
|
|
356
|
+
properties: {
|
|
357
|
+
url: {
|
|
358
|
+
type: "string",
|
|
359
|
+
description: "The target URL to audit (e.g. https://example.com).",
|
|
360
|
+
},
|
|
361
|
+
max_depth: {
|
|
362
|
+
type: "number",
|
|
363
|
+
description: "Maximum crawl depth. Default: 2.",
|
|
364
|
+
default: 2,
|
|
365
|
+
},
|
|
366
|
+
max_pages: {
|
|
367
|
+
type: "number",
|
|
368
|
+
description: "Maximum pages to audit. Default: 30.",
|
|
369
|
+
default: 30,
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
required: ["url"],
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
];
|
|
376
|
+
|
|
377
|
+
// ── MCP Server setup ─────────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
const server = new Server(
|
|
380
|
+
{ name: "secura", version: "0.1.0" },
|
|
381
|
+
{ capabilities: { tools: {} } },
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
385
|
+
|
|
386
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
387
|
+
const { name, arguments: args } = request.params;
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
switch (name) {
|
|
391
|
+
case "run_static_scan": return await runStaticScan(args);
|
|
392
|
+
case "run_recon": return await runRecon(args);
|
|
393
|
+
case "run_header_audit": return await runHeaderAudit(args);
|
|
394
|
+
default:
|
|
395
|
+
return {
|
|
396
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
397
|
+
isError: true,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
} catch (err) {
|
|
401
|
+
return {
|
|
402
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
403
|
+
isError: true,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// ── Start ─────────────────────────────────────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
const transport = new StdioServerTransport();
|
|
411
|
+
await server.connect(transport);
|
|
412
|
+
// Server runs until the editor closes stdin.
|
package/package.json
CHANGED
|
@@ -1,46 +1,54 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@authtrack/secura",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"secura": "bin/secura.mjs"
|
|
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
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@authtrack/secura",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "SecuraAI security scanner CLI and MCP server — static analysis, recon, header audit and injection testing via AI agents.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"secura": "bin/secura.mjs",
|
|
8
|
+
"secura-mcp": "bin/secura-mcp.mjs"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin",
|
|
15
|
+
"src",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"sast",
|
|
23
|
+
"static-analysis",
|
|
24
|
+
"security",
|
|
25
|
+
"semgrep",
|
|
26
|
+
"codeql",
|
|
27
|
+
"gitleaks",
|
|
28
|
+
"osv-scanner",
|
|
29
|
+
"bearer",
|
|
30
|
+
"cli",
|
|
31
|
+
"vulnerability",
|
|
32
|
+
"mcp",
|
|
33
|
+
"model-context-protocol",
|
|
34
|
+
"securai",
|
|
35
|
+
"securaai"
|
|
36
|
+
],
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/ashleyalmeida07/Authtrack-Major-Project.git",
|
|
40
|
+
"directory": "cli"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/ashleyalmeida07/Authtrack-Major-Project#readme",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/ashleyalmeida07/Authtrack-Major-Project/issues"
|
|
45
|
+
},
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"secura": "node bin/secura.mjs",
|
|
52
|
+
"secura-mcp": "node bin/secura-mcp.mjs"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/config.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
// Matches frontend/src/lib/api.ts (API_BASE) so the CLI and the web app default
|
|
10
10
|
// to the same backend.
|
|
11
|
-
const DEFAULT_API = "
|
|
11
|
+
const DEFAULT_API = "https://securaai-major-project.onrender.com/api/v1";
|
|
12
12
|
|
|
13
13
|
export function resolveConfig(values) {
|
|
14
14
|
const raw = values.api || process.env.SECURA_API || DEFAULT_API;
|