sastai 1.0.0
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 +27 -0
- package/bin/cli.js +194 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# sastai
|
|
2
|
+
|
|
3
|
+
AI-powered **SAST** engine launcher, by **Insomnia**. See [insom.ai](https://insom.ai).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install -g sastai
|
|
7
|
+
sastai scan ./my-project
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
On first use, `sastai` detects your OS and CPU architecture (Windows/Linux/macOS,
|
|
11
|
+
x64 & arm64), downloads the matching native engine from
|
|
12
|
+
[insom.ai](https://insom.ai), verifies it with SHA-256, caches it under
|
|
13
|
+
`~/.insom/engine`, and checks for updates once a day. All arguments are passed
|
|
14
|
+
straight through to the engine — the same model as the `sast` package on PyPI,
|
|
15
|
+
delivered for the Node / npm ecosystem.
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
| Command | Description |
|
|
20
|
+
|--------------------|------------------------------------------|
|
|
21
|
+
| `sastai scan <path>` | Run a SAST scan over a project |
|
|
22
|
+
| `sastai --version` | Print the engine version |
|
|
23
|
+
| `sastai help` | Show launcher help |
|
|
24
|
+
|
|
25
|
+
## License
|
|
26
|
+
|
|
27
|
+
UNLICENSED — proprietary. © Insomnia / insom.ai.
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const https = require("https");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const os = require("os");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const crypto = require("crypto");
|
|
9
|
+
const { spawnSync } = require("child_process");
|
|
10
|
+
|
|
11
|
+
const pkg = require("../package.json");
|
|
12
|
+
|
|
13
|
+
const MANIFEST_URL = "https://insom.ai/api/plugin/manifest";
|
|
14
|
+
const DOWNLOAD_BASE = "https://insom.ai/static/downloads/";
|
|
15
|
+
const HOME_DIR = path.join(os.homedir(), ".insom", "engine");
|
|
16
|
+
const MARKER = path.join(HOME_DIR, "current.json");
|
|
17
|
+
const UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000; // daily check, like the pip launcher
|
|
18
|
+
|
|
19
|
+
function platformKey() {
|
|
20
|
+
const p = process.platform;
|
|
21
|
+
const a = process.arch;
|
|
22
|
+
if (p === "win32") return "windows";
|
|
23
|
+
if (p === "linux") return a === "arm64" ? "linux-arm64" : "linux";
|
|
24
|
+
if (p === "darwin") return a === "arm64" ? "macos-arm64" : "macos";
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function httpGet(url) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
https
|
|
31
|
+
.get(url, { headers: { "User-Agent": `sastai-npm/${pkg.version}` } }, (res) => {
|
|
32
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
33
|
+
res.resume();
|
|
34
|
+
return resolve(httpGet(res.headers.location));
|
|
35
|
+
}
|
|
36
|
+
if (res.statusCode !== 200) {
|
|
37
|
+
res.resume();
|
|
38
|
+
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
39
|
+
}
|
|
40
|
+
const chunks = [];
|
|
41
|
+
res.on("data", (c) => chunks.push(c));
|
|
42
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
43
|
+
})
|
|
44
|
+
.on("error", reject);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function download(url, dest) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const file = fs.createWriteStream(dest);
|
|
51
|
+
const req = https.get(
|
|
52
|
+
url,
|
|
53
|
+
{ headers: { "User-Agent": `sastai-npm/${pkg.version}` } },
|
|
54
|
+
(res) => {
|
|
55
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
56
|
+
res.resume();
|
|
57
|
+
file.close();
|
|
58
|
+
return resolve(download(res.headers.location, dest));
|
|
59
|
+
}
|
|
60
|
+
if (res.statusCode !== 200) {
|
|
61
|
+
res.resume();
|
|
62
|
+
file.close();
|
|
63
|
+
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
64
|
+
}
|
|
65
|
+
const total = parseInt(res.headers["content-length"] || "0", 10);
|
|
66
|
+
let got = 0;
|
|
67
|
+
res.on("data", (c) => {
|
|
68
|
+
got += c.length;
|
|
69
|
+
if (total && process.stderr.isTTY) {
|
|
70
|
+
const pct = ((got / total) * 100).toFixed(0);
|
|
71
|
+
process.stderr.write(`\r downloading engine… ${pct}%`);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
res.pipe(file);
|
|
75
|
+
file.on("finish", () => file.close(() => {
|
|
76
|
+
if (process.stderr.isTTY) process.stderr.write("\r downloading engine… done \n");
|
|
77
|
+
resolve();
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
req.on("error", (e) => {
|
|
82
|
+
file.close();
|
|
83
|
+
reject(e);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sha256(file) {
|
|
89
|
+
const h = crypto.createHash("sha256");
|
|
90
|
+
h.update(fs.readFileSync(file));
|
|
91
|
+
return h.digest("hex");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readMarker() {
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(fs.readFileSync(MARKER, "utf8"));
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function ensureEngine() {
|
|
103
|
+
const key = platformKey();
|
|
104
|
+
if (!key) {
|
|
105
|
+
throw new Error(`Unsupported platform: ${process.platform}/${process.arch}`);
|
|
106
|
+
}
|
|
107
|
+
fs.mkdirSync(HOME_DIR, { recursive: true });
|
|
108
|
+
|
|
109
|
+
const marker = readMarker();
|
|
110
|
+
const haveBinary = marker && fs.existsSync(path.join(HOME_DIR, marker.filename));
|
|
111
|
+
const fresh = marker && Date.now() - (marker.checkedAt || 0) < UPDATE_INTERVAL_MS;
|
|
112
|
+
|
|
113
|
+
// Use cached binary without hitting the network if it's fresh.
|
|
114
|
+
if (haveBinary && fresh) return path.join(HOME_DIR, marker.filename);
|
|
115
|
+
|
|
116
|
+
let entry;
|
|
117
|
+
try {
|
|
118
|
+
const manifest = JSON.parse((await httpGet(MANIFEST_URL)).toString("utf8"));
|
|
119
|
+
entry = manifest.sast && manifest.sast[key];
|
|
120
|
+
} catch (e) {
|
|
121
|
+
// Offline: fall back to whatever we already have.
|
|
122
|
+
if (haveBinary) return path.join(HOME_DIR, marker.filename);
|
|
123
|
+
throw new Error(`Could not fetch manifest and no cached engine present: ${e.message}`);
|
|
124
|
+
}
|
|
125
|
+
if (!entry) throw new Error(`No engine build for platform "${key}"`);
|
|
126
|
+
|
|
127
|
+
const dest = path.join(HOME_DIR, entry.filename);
|
|
128
|
+
const upToDate =
|
|
129
|
+
fs.existsSync(dest) && marker && marker.sha256 === entry.sha256;
|
|
130
|
+
|
|
131
|
+
if (!upToDate) {
|
|
132
|
+
await download(DOWNLOAD_BASE + entry.filename, dest);
|
|
133
|
+
const got = sha256(dest);
|
|
134
|
+
if (got !== entry.sha256) {
|
|
135
|
+
fs.unlinkSync(dest);
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Checksum mismatch for ${entry.filename}\n expected ${entry.sha256}\n got ${got}`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (process.platform !== "win32") fs.chmodSync(dest, 0o755);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fs.writeFileSync(
|
|
144
|
+
MARKER,
|
|
145
|
+
JSON.stringify(
|
|
146
|
+
{ key, filename: entry.filename, sha256: entry.sha256, version: entry.version, checkedAt: Date.now() },
|
|
147
|
+
null,
|
|
148
|
+
2
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
return dest;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function main() {
|
|
155
|
+
const argv = process.argv.slice(2);
|
|
156
|
+
|
|
157
|
+
if (argv[0] === "--launcher-version") {
|
|
158
|
+
process.stdout.write(pkg.version + "\n");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (argv[0] === "help" && argv.length === 1) {
|
|
162
|
+
process.stdout.write(
|
|
163
|
+
[
|
|
164
|
+
" sastai — AI-powered SAST engine launcher (insom.ai)",
|
|
165
|
+
"",
|
|
166
|
+
" Usage: sastai <command> [options] (passed through to the engine)",
|
|
167
|
+
" sastai scan <path>",
|
|
168
|
+
" sastai --version",
|
|
169
|
+
"",
|
|
170
|
+
` Launcher v${pkg.version}. Engine is downloaded on first use to ~/.insom/engine.`,
|
|
171
|
+
" Docs & downloads: https://insom.ai",
|
|
172
|
+
"",
|
|
173
|
+
].join("\n")
|
|
174
|
+
);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let engine;
|
|
179
|
+
try {
|
|
180
|
+
engine = await ensureEngine();
|
|
181
|
+
} catch (e) {
|
|
182
|
+
process.stderr.write(` sastai: ${e.message}\n See https://insom.ai\n`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const res = spawnSync(engine, argv, { stdio: "inherit" });
|
|
187
|
+
if (res.error) {
|
|
188
|
+
process.stderr.write(` sastai: failed to launch engine: ${res.error.message}\n`);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
process.exit(res.status === null ? 1 : res.status);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sastai",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "sastai — AI-powered SAST engine launcher (Insomnia / insom.ai)",
|
|
5
|
+
"bin": {
|
|
6
|
+
"sastai": "bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "node bin/cli.js --launcher-version"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"sast",
|
|
13
|
+
"sast-ai",
|
|
14
|
+
"ai",
|
|
15
|
+
"security",
|
|
16
|
+
"static-analysis",
|
|
17
|
+
"insomnia",
|
|
18
|
+
"insom",
|
|
19
|
+
"vulnerability-scanner",
|
|
20
|
+
"appsec",
|
|
21
|
+
"sdlc",
|
|
22
|
+
"cicd"
|
|
23
|
+
],
|
|
24
|
+
"homepage": "https://insom.ai",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://insom.ai"
|
|
27
|
+
},
|
|
28
|
+
"author": "Insomnia (insom.ai)",
|
|
29
|
+
"license": "UNLICENSED",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=14"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"bin/",
|
|
35
|
+
"README.md"
|
|
36
|
+
]
|
|
37
|
+
}
|