@oculum/cli 1.0.9 → 1.0.11
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/dist/index.js +494 -139
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -40915,8 +40915,8 @@ var {
|
|
|
40915
40915
|
} = import_index.default;
|
|
40916
40916
|
|
|
40917
40917
|
// src/commands/scan.ts
|
|
40918
|
-
var
|
|
40919
|
-
var
|
|
40918
|
+
var import_path4 = require("path");
|
|
40919
|
+
var import_fs5 = require("fs");
|
|
40920
40920
|
init_esm7();
|
|
40921
40921
|
|
|
40922
40922
|
// node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
@@ -43313,6 +43313,16 @@ function getApiBaseUrl() {
|
|
|
43313
43313
|
function setAuthCredentials(apiKey, email, tier) {
|
|
43314
43314
|
updateConfig({ apiKey, email, tier });
|
|
43315
43315
|
}
|
|
43316
|
+
function syncAuthFromVerification(verifyResponse) {
|
|
43317
|
+
if (!verifyResponse.valid) return;
|
|
43318
|
+
const config = getConfig();
|
|
43319
|
+
if (!config.apiKey) return;
|
|
43320
|
+
const email = verifyResponse.email || config.email;
|
|
43321
|
+
const tier = verifyResponse.tier || config.tier || "free";
|
|
43322
|
+
if (tier !== config.tier || email !== config.email) {
|
|
43323
|
+
setAuthCredentials(config.apiKey, email, tier);
|
|
43324
|
+
}
|
|
43325
|
+
}
|
|
43316
43326
|
|
|
43317
43327
|
// src/utils/api.ts
|
|
43318
43328
|
var APIError = class extends Error {
|
|
@@ -43872,8 +43882,83 @@ function validateConfig(config) {
|
|
|
43872
43882
|
validated.watch.clear = config.watch.clear;
|
|
43873
43883
|
}
|
|
43874
43884
|
}
|
|
43885
|
+
if (config.profiles && typeof config.profiles === "object") {
|
|
43886
|
+
validated.profiles = config.profiles;
|
|
43887
|
+
}
|
|
43888
|
+
if (typeof config.defaultProfile === "string") {
|
|
43889
|
+
validated.defaultProfile = config.defaultProfile;
|
|
43890
|
+
}
|
|
43875
43891
|
return validated;
|
|
43876
43892
|
}
|
|
43893
|
+
function getProfileConfig(projectConfig, profileName) {
|
|
43894
|
+
if (!projectConfig) return null;
|
|
43895
|
+
const name = profileName || projectConfig.defaultProfile;
|
|
43896
|
+
if (!name) return null;
|
|
43897
|
+
const profile = projectConfig.profiles?.[name];
|
|
43898
|
+
if (!profile) {
|
|
43899
|
+
if (profileName) {
|
|
43900
|
+
console.warn(source_default.yellow(`Warning: Profile "${profileName}" not found in config`));
|
|
43901
|
+
}
|
|
43902
|
+
return null;
|
|
43903
|
+
}
|
|
43904
|
+
return profile;
|
|
43905
|
+
}
|
|
43906
|
+
|
|
43907
|
+
// src/utils/history.ts
|
|
43908
|
+
var import_fs4 = require("fs");
|
|
43909
|
+
var import_path3 = require("path");
|
|
43910
|
+
var import_os2 = require("os");
|
|
43911
|
+
var import_crypto = require("crypto");
|
|
43912
|
+
var CONFIG_DIR2 = (0, import_path3.join)((0, import_os2.homedir)(), ".oculum");
|
|
43913
|
+
var HISTORY_FILE = (0, import_path3.join)(CONFIG_DIR2, "history.json");
|
|
43914
|
+
var MAX_ENTRIES = 25;
|
|
43915
|
+
function ensureConfigDir2() {
|
|
43916
|
+
if (!(0, import_fs4.existsSync)(CONFIG_DIR2)) {
|
|
43917
|
+
(0, import_fs4.mkdirSync)(CONFIG_DIR2, { recursive: true });
|
|
43918
|
+
}
|
|
43919
|
+
}
|
|
43920
|
+
function readHistoryFile() {
|
|
43921
|
+
try {
|
|
43922
|
+
if (!(0, import_fs4.existsSync)(HISTORY_FILE)) return [];
|
|
43923
|
+
const content = (0, import_fs4.readFileSync)(HISTORY_FILE, "utf-8");
|
|
43924
|
+
const parsed = JSON.parse(content);
|
|
43925
|
+
if (!Array.isArray(parsed)) return [];
|
|
43926
|
+
return parsed;
|
|
43927
|
+
} catch {
|
|
43928
|
+
return [];
|
|
43929
|
+
}
|
|
43930
|
+
}
|
|
43931
|
+
function writeHistoryFile(entries) {
|
|
43932
|
+
ensureConfigDir2();
|
|
43933
|
+
(0, import_fs4.writeFileSync)(HISTORY_FILE, JSON.stringify(entries, null, 2));
|
|
43934
|
+
}
|
|
43935
|
+
function listScanHistory() {
|
|
43936
|
+
return readHistoryFile();
|
|
43937
|
+
}
|
|
43938
|
+
function getScanHistoryEntry(id) {
|
|
43939
|
+
return readHistoryFile().find((e2) => e2.id === id);
|
|
43940
|
+
}
|
|
43941
|
+
function addScanHistoryEntry(input) {
|
|
43942
|
+
const entry = {
|
|
43943
|
+
id: input.id ?? (0, import_crypto.randomUUID)(),
|
|
43944
|
+
createdAt: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
43945
|
+
targetPath: input.targetPath,
|
|
43946
|
+
options: input.options,
|
|
43947
|
+
result: input.result
|
|
43948
|
+
};
|
|
43949
|
+
const current = readHistoryFile();
|
|
43950
|
+
const updated = [entry, ...current].slice(0, MAX_ENTRIES);
|
|
43951
|
+
writeHistoryFile(updated);
|
|
43952
|
+
return entry;
|
|
43953
|
+
}
|
|
43954
|
+
function deleteScanHistoryEntry(id) {
|
|
43955
|
+
const current = readHistoryFile();
|
|
43956
|
+
const updated = current.filter((e2) => e2.id !== id);
|
|
43957
|
+
writeHistoryFile(updated);
|
|
43958
|
+
}
|
|
43959
|
+
function clearScanHistory() {
|
|
43960
|
+
writeHistoryFile([]);
|
|
43961
|
+
}
|
|
43877
43962
|
|
|
43878
43963
|
// src/commands/scan.ts
|
|
43879
43964
|
function getChangedFiles(absolutePath, diff) {
|
|
@@ -43890,22 +43975,22 @@ function getChangedFiles(absolutePath, diff) {
|
|
|
43890
43975
|
args = ["diff", "--name-only", "HEAD"];
|
|
43891
43976
|
}
|
|
43892
43977
|
const out = (0, import_child_process.execFileSync)("git", args, { cwd: gitRoot, stdio: ["ignore", "pipe", "ignore"] }).toString();
|
|
43893
|
-
const files = out.split("\n").map((s) => s.trim()).filter(Boolean).map((p2) => (0,
|
|
43978
|
+
const files = out.split("\n").map((s) => s.trim()).filter(Boolean).map((p2) => (0, import_path4.resolve)(gitRoot, p2)).filter((p2) => p2.startsWith(absolutePath));
|
|
43894
43979
|
return files;
|
|
43895
43980
|
} catch {
|
|
43896
43981
|
return [];
|
|
43897
43982
|
}
|
|
43898
43983
|
}
|
|
43899
43984
|
function isScannableFile(filePath) {
|
|
43900
|
-
const ext2 = (0,
|
|
43901
|
-
const fileName = (0,
|
|
43985
|
+
const ext2 = (0, import_path4.extname)(filePath).toLowerCase();
|
|
43986
|
+
const fileName = (0, import_path4.basename)(filePath);
|
|
43902
43987
|
if (import_scanner.SPECIAL_FILES.includes(fileName)) {
|
|
43903
43988
|
return true;
|
|
43904
43989
|
}
|
|
43905
43990
|
return import_scanner.SCANNABLE_EXTENSIONS.includes(ext2);
|
|
43906
43991
|
}
|
|
43907
43992
|
function getLanguage(filePath) {
|
|
43908
|
-
const ext2 = (0,
|
|
43993
|
+
const ext2 = (0, import_path4.extname)(filePath).toLowerCase();
|
|
43909
43994
|
const langMap = {
|
|
43910
43995
|
".js": "javascript",
|
|
43911
43996
|
".jsx": "javascript",
|
|
@@ -43930,15 +44015,15 @@ function getLanguage(filePath) {
|
|
|
43930
44015
|
return langMap[ext2] || "text";
|
|
43931
44016
|
}
|
|
43932
44017
|
async function collectFiles(targetPath) {
|
|
43933
|
-
const absolutePath = (0,
|
|
43934
|
-
const stats = (0,
|
|
44018
|
+
const absolutePath = (0, import_path4.resolve)(targetPath);
|
|
44019
|
+
const stats = (0, import_fs5.statSync)(absolutePath);
|
|
43935
44020
|
const files = [];
|
|
43936
44021
|
if (stats.isFile()) {
|
|
43937
44022
|
if (isScannableFile(absolutePath)) {
|
|
43938
|
-
const content = (0,
|
|
44023
|
+
const content = (0, import_fs5.readFileSync)(absolutePath, "utf-8");
|
|
43939
44024
|
if (content.length <= import_scanner.MAX_FILE_SIZE) {
|
|
43940
44025
|
files.push({
|
|
43941
|
-
path: (0,
|
|
44026
|
+
path: (0, import_path4.relative)(process.cwd(), absolutePath),
|
|
43942
44027
|
content,
|
|
43943
44028
|
language: getLanguage(absolutePath),
|
|
43944
44029
|
size: content.length
|
|
@@ -44007,8 +44092,8 @@ async function collectFiles(targetPath) {
|
|
|
44007
44092
|
"**/.env.*.local",
|
|
44008
44093
|
"**/**.env"
|
|
44009
44094
|
];
|
|
44010
|
-
const gitignorePath = (0,
|
|
44011
|
-
const hasGitignore = (0,
|
|
44095
|
+
const gitignorePath = (0, import_path4.join)(absolutePath, ".gitignore");
|
|
44096
|
+
const hasGitignore = (0, import_fs5.existsSync)(gitignorePath);
|
|
44012
44097
|
const globOptions = {
|
|
44013
44098
|
cwd: absolutePath,
|
|
44014
44099
|
ignore: ignorePatterns,
|
|
@@ -44022,11 +44107,11 @@ async function collectFiles(targetPath) {
|
|
|
44022
44107
|
for (const file of foundFiles) {
|
|
44023
44108
|
const filePath = String(file);
|
|
44024
44109
|
try {
|
|
44025
|
-
const fileStats = (0,
|
|
44110
|
+
const fileStats = (0, import_fs5.statSync)(filePath);
|
|
44026
44111
|
if (fileStats.size > import_scanner.MAX_FILE_SIZE) continue;
|
|
44027
|
-
const content = (0,
|
|
44112
|
+
const content = (0, import_fs5.readFileSync)(filePath, "utf-8");
|
|
44028
44113
|
files.push({
|
|
44029
|
-
path: (0,
|
|
44114
|
+
path: (0, import_path4.relative)(process.cwd(), filePath),
|
|
44030
44115
|
content,
|
|
44031
44116
|
language: getLanguage(filePath),
|
|
44032
44117
|
size: content.length
|
|
@@ -44038,8 +44123,8 @@ async function collectFiles(targetPath) {
|
|
|
44038
44123
|
return files;
|
|
44039
44124
|
}
|
|
44040
44125
|
async function collectFilesForScan(targetPath, options) {
|
|
44041
|
-
const absolutePath = (0,
|
|
44042
|
-
const stats = (0,
|
|
44126
|
+
const absolutePath = (0, import_path4.resolve)(targetPath);
|
|
44127
|
+
const stats = (0, import_fs5.statSync)(absolutePath);
|
|
44043
44128
|
if (stats.isFile()) {
|
|
44044
44129
|
return collectFiles(targetPath);
|
|
44045
44130
|
}
|
|
@@ -44048,13 +44133,13 @@ async function collectFilesForScan(targetPath, options) {
|
|
|
44048
44133
|
const files = [];
|
|
44049
44134
|
for (const filePath of changed) {
|
|
44050
44135
|
try {
|
|
44051
|
-
if (!(0,
|
|
44136
|
+
if (!(0, import_fs5.existsSync)(filePath)) continue;
|
|
44052
44137
|
if (!isScannableFile(filePath)) continue;
|
|
44053
|
-
const fileStats = (0,
|
|
44138
|
+
const fileStats = (0, import_fs5.statSync)(filePath);
|
|
44054
44139
|
if (fileStats.size > import_scanner.MAX_FILE_SIZE) continue;
|
|
44055
|
-
const content = (0,
|
|
44140
|
+
const content = (0, import_fs5.readFileSync)(filePath, "utf-8");
|
|
44056
44141
|
files.push({
|
|
44057
|
-
path: (0,
|
|
44142
|
+
path: (0, import_path4.relative)(process.cwd(), filePath),
|
|
44058
44143
|
content,
|
|
44059
44144
|
language: getLanguage(filePath),
|
|
44060
44145
|
size: content.length
|
|
@@ -44068,7 +44153,7 @@ async function collectFilesForScan(targetPath, options) {
|
|
|
44068
44153
|
}
|
|
44069
44154
|
function createEmptyResult(targetPath) {
|
|
44070
44155
|
return {
|
|
44071
|
-
repoName: (0,
|
|
44156
|
+
repoName: (0, import_path4.basename)((0, import_path4.resolve)(targetPath)),
|
|
44072
44157
|
repoUrl: "",
|
|
44073
44158
|
branch: "local",
|
|
44074
44159
|
filesScanned: 0,
|
|
@@ -44192,6 +44277,13 @@ async function runScanOnce(targetPath, options) {
|
|
|
44192
44277
|
const config = getConfig();
|
|
44193
44278
|
const noColor = options.color === false;
|
|
44194
44279
|
const cancellationToken = (0, import_scanner.createCancellationToken)();
|
|
44280
|
+
if ((options.depth === "validated" || options.depth === "deep") && isAuthenticated()) {
|
|
44281
|
+
try {
|
|
44282
|
+
const verified = await verifyApiKey(config.apiKey);
|
|
44283
|
+
syncAuthFromVerification(verified);
|
|
44284
|
+
} catch {
|
|
44285
|
+
}
|
|
44286
|
+
}
|
|
44195
44287
|
if ((options.depth === "validated" || options.depth === "deep") && !isAuthenticated()) {
|
|
44196
44288
|
if (!options.quiet) {
|
|
44197
44289
|
console.log("");
|
|
@@ -44283,7 +44375,7 @@ async function runScanOnce(targetPath, options) {
|
|
|
44283
44375
|
options.depth,
|
|
44284
44376
|
config.apiKey,
|
|
44285
44377
|
{
|
|
44286
|
-
name: (0,
|
|
44378
|
+
name: (0, import_path4.basename)((0, import_path4.resolve)(targetPath)),
|
|
44287
44379
|
url: "",
|
|
44288
44380
|
branch: "local"
|
|
44289
44381
|
}
|
|
@@ -44295,7 +44387,7 @@ async function runScanOnce(targetPath, options) {
|
|
|
44295
44387
|
result = await (0, import_scanner.runScan)(
|
|
44296
44388
|
files,
|
|
44297
44389
|
{
|
|
44298
|
-
name: (0,
|
|
44390
|
+
name: (0, import_path4.basename)((0, import_path4.resolve)(targetPath)),
|
|
44299
44391
|
url: "",
|
|
44300
44392
|
branch: "local"
|
|
44301
44393
|
},
|
|
@@ -44358,32 +44450,33 @@ Results written to ${options.output}`));
|
|
|
44358
44450
|
}
|
|
44359
44451
|
async function runScan(targetPath, cliOptions) {
|
|
44360
44452
|
const projectConfig = loadProjectConfig(targetPath, cliOptions.quiet);
|
|
44361
|
-
|
|
44453
|
+
const profileConfig = getProfileConfig(projectConfig, cliOptions.profile);
|
|
44454
|
+
let scanDepth = cliOptions.mode || cliOptions.depth;
|
|
44362
44455
|
if (scanDepth === "quick") {
|
|
44363
44456
|
scanDepth = "cheap";
|
|
44364
44457
|
}
|
|
44365
|
-
const
|
|
44366
|
-
depth: scanDepth,
|
|
44367
|
-
format: cliOptions.format ?? "terminal",
|
|
44368
|
-
failOn: cliOptions.failOn ?? "high",
|
|
44458
|
+
const options = {
|
|
44459
|
+
depth: scanDepth ?? profileConfig?.depth ?? projectConfig?.depth ?? "cheap",
|
|
44460
|
+
format: cliOptions.format ?? profileConfig?.format ?? projectConfig?.format ?? "terminal",
|
|
44461
|
+
failOn: cliOptions.failOn ?? profileConfig?.failOn ?? projectConfig?.failOn ?? "high",
|
|
44369
44462
|
color: cliOptions.color,
|
|
44370
|
-
quiet: cliOptions.quiet,
|
|
44463
|
+
quiet: cliOptions.quiet ?? profileConfig?.quiet ?? projectConfig?.quiet,
|
|
44371
44464
|
verbose: cliOptions.verbose,
|
|
44372
44465
|
incremental: cliOptions.incremental,
|
|
44373
44466
|
diff: cliOptions.diff,
|
|
44374
|
-
output: cliOptions.output
|
|
44375
|
-
|
|
44376
|
-
const options = {
|
|
44377
|
-
...baseOptions,
|
|
44378
|
-
depth: baseOptions.depth ?? projectConfig?.depth ?? "cheap",
|
|
44379
|
-
format: baseOptions.format ?? projectConfig?.format ?? "terminal",
|
|
44380
|
-
failOn: baseOptions.failOn ?? projectConfig?.failOn ?? "high",
|
|
44381
|
-
quiet: baseOptions.quiet ?? projectConfig?.quiet,
|
|
44382
|
-
verbose: baseOptions.verbose,
|
|
44383
|
-
output: baseOptions.output ?? projectConfig?.output
|
|
44467
|
+
output: cliOptions.output ?? profileConfig?.output ?? projectConfig?.output,
|
|
44468
|
+
profile: cliOptions.profile
|
|
44384
44469
|
};
|
|
44385
44470
|
try {
|
|
44386
|
-
const { output, exitCode } = await runScanOnce(targetPath, options);
|
|
44471
|
+
const { result, output, exitCode } = await runScanOnce(targetPath, options);
|
|
44472
|
+
try {
|
|
44473
|
+
addScanHistoryEntry({
|
|
44474
|
+
targetPath,
|
|
44475
|
+
options: { depth: options.depth, format: options.format, failOn: options.failOn },
|
|
44476
|
+
result
|
|
44477
|
+
});
|
|
44478
|
+
} catch {
|
|
44479
|
+
}
|
|
44387
44480
|
console.log(output);
|
|
44388
44481
|
if (exitCode !== 0) {
|
|
44389
44482
|
process.exit(exitCode);
|
|
@@ -44394,7 +44487,7 @@ async function runScan(targetPath, cliOptions) {
|
|
|
44394
44487
|
process.exit(1);
|
|
44395
44488
|
}
|
|
44396
44489
|
}
|
|
44397
|
-
var scanCommand = new Command("scan").description("Scan a directory or file for security vulnerabilities").argument("[path]", "path to scan", ".").option("-d, --depth <depth>", "scan depth: cheap (free), validated, deep"
|
|
44490
|
+
var scanCommand = new Command("scan").description("Scan a directory or file for security vulnerabilities").argument("[path]", "path to scan", ".").option("-d, --depth <depth>", "scan depth: cheap (free), validated, deep").option("-m, --mode <mode>", "alias for --depth (quick, validated, deep)").option("-f, --format <format>", "output format: terminal, json, sarif, markdown").option("--fail-on <severity>", "exit with error code if findings at severity").option("--no-color", "disable colored output").option("-q, --quiet", "minimal output for CI (suppress spinners and decorations)").option("-v, --verbose", "show detailed scanner logs (debug mode)").option("-o, --output <file>", "write output to file").option("-p, --profile <name>", "use named profile from oculum.config.json").option("--incremental", "only scan changed files (requires git)").option("--diff <ref>", "diff against branch/commit for incremental scan").addHelpText("after", `
|
|
44398
44491
|
Scan Modes:
|
|
44399
44492
|
cheap Free Fast pattern matching, runs locally
|
|
44400
44493
|
Best for: Quick checks, CI/CD pipelines
|
|
@@ -44419,9 +44512,14 @@ Configuration:
|
|
|
44419
44512
|
{
|
|
44420
44513
|
"depth": "validated",
|
|
44421
44514
|
"failOn": "high",
|
|
44422
|
-
"
|
|
44515
|
+
"profiles": {
|
|
44516
|
+
"ci": { "depth": "validated", "quiet": true, "format": "sarif" },
|
|
44517
|
+
"strict": { "depth": "validated", "failOn": "medium" }
|
|
44518
|
+
}
|
|
44423
44519
|
}
|
|
44424
44520
|
|
|
44521
|
+
Use profiles: oculum scan . --profile ci
|
|
44522
|
+
|
|
44425
44523
|
More Help:
|
|
44426
44524
|
$ oculum help scan-modes Detailed mode comparison
|
|
44427
44525
|
$ oculum help ci-setup CI/CD integration examples
|
|
@@ -44551,7 +44649,10 @@ async function status() {
|
|
|
44551
44649
|
spinner.succeed(" Authenticated");
|
|
44552
44650
|
console.log("");
|
|
44553
44651
|
const email = result.email || config.email || "unknown";
|
|
44554
|
-
const tier = result.tier ||
|
|
44652
|
+
const tier = result.tier || "free";
|
|
44653
|
+
if (tier !== config.tier || email !== config.email) {
|
|
44654
|
+
setAuthCredentials(config.apiKey, email, tier);
|
|
44655
|
+
}
|
|
44555
44656
|
const tierBadge = tier === "pro" ? source_default.bgBlue.white(" PRO ") : tier === "enterprise" ? source_default.bgMagenta.white(" ENTERPRISE ") : source_default.bgGray.white(" FREE ");
|
|
44556
44657
|
console.log(source_default.dim(" Email: ") + source_default.white(email));
|
|
44557
44658
|
console.log(source_default.dim(" Plan: ") + tierBadge);
|
|
@@ -44595,11 +44696,12 @@ var statusCommand = new Command("status").description("Show current authenticati
|
|
|
44595
44696
|
var upgradeCommand = new Command("upgrade").description("Upgrade your subscription").action(upgrade);
|
|
44596
44697
|
|
|
44597
44698
|
// src/commands/watch.ts
|
|
44598
|
-
var
|
|
44599
|
-
var
|
|
44699
|
+
var import_path5 = require("path");
|
|
44700
|
+
var import_fs8 = require("fs");
|
|
44701
|
+
var readline = __toESM(require("readline"));
|
|
44600
44702
|
|
|
44601
44703
|
// ../../node_modules/chokidar/esm/index.js
|
|
44602
|
-
var
|
|
44704
|
+
var import_fs7 = require("fs");
|
|
44603
44705
|
var import_promises4 = require("fs/promises");
|
|
44604
44706
|
var import_events = require("events");
|
|
44605
44707
|
var sysPath2 = __toESM(require("path"), 1);
|
|
@@ -44824,10 +44926,10 @@ function readdirp(root, options = {}) {
|
|
|
44824
44926
|
}
|
|
44825
44927
|
|
|
44826
44928
|
// ../../node_modules/chokidar/esm/handler.js
|
|
44827
|
-
var
|
|
44929
|
+
var import_fs6 = require("fs");
|
|
44828
44930
|
var import_promises3 = require("fs/promises");
|
|
44829
44931
|
var sysPath = __toESM(require("path"), 1);
|
|
44830
|
-
var
|
|
44932
|
+
var import_os3 = require("os");
|
|
44831
44933
|
var STR_DATA = "data";
|
|
44832
44934
|
var STR_END = "end";
|
|
44833
44935
|
var STR_CLOSE = "close";
|
|
@@ -44838,7 +44940,7 @@ var isWindows = pl === "win32";
|
|
|
44838
44940
|
var isMacos = pl === "darwin";
|
|
44839
44941
|
var isLinux = pl === "linux";
|
|
44840
44942
|
var isFreeBSD = pl === "freebsd";
|
|
44841
|
-
var isIBMi = (0,
|
|
44943
|
+
var isIBMi = (0, import_os3.type)() === "OS400";
|
|
44842
44944
|
var EVENTS = {
|
|
44843
44945
|
ALL: "all",
|
|
44844
44946
|
READY: "ready",
|
|
@@ -45162,7 +45264,7 @@ function createFsWatchInstance(path2, options, listener, errHandler, emitRaw) {
|
|
|
45162
45264
|
}
|
|
45163
45265
|
};
|
|
45164
45266
|
try {
|
|
45165
|
-
return (0,
|
|
45267
|
+
return (0, import_fs6.watch)(path2, {
|
|
45166
45268
|
persistent: options.persistent
|
|
45167
45269
|
}, handleEvent);
|
|
45168
45270
|
} catch (error) {
|
|
@@ -45245,7 +45347,7 @@ var setFsWatchFileListener = (path2, fullPath, options, handlers) => {
|
|
|
45245
45347
|
let cont = FsWatchFileInstances.get(fullPath);
|
|
45246
45348
|
const copts = cont && cont.options;
|
|
45247
45349
|
if (copts && (copts.persistent < options.persistent || copts.interval > options.interval)) {
|
|
45248
|
-
(0,
|
|
45350
|
+
(0, import_fs6.unwatchFile)(fullPath);
|
|
45249
45351
|
cont = void 0;
|
|
45250
45352
|
}
|
|
45251
45353
|
if (cont) {
|
|
@@ -45256,7 +45358,7 @@ var setFsWatchFileListener = (path2, fullPath, options, handlers) => {
|
|
|
45256
45358
|
listeners: listener,
|
|
45257
45359
|
rawEmitters: rawEmitter,
|
|
45258
45360
|
options,
|
|
45259
|
-
watcher: (0,
|
|
45361
|
+
watcher: (0, import_fs6.watchFile)(fullPath, options, (curr, prev) => {
|
|
45260
45362
|
foreach(cont.rawEmitters, (rawEmitter2) => {
|
|
45261
45363
|
rawEmitter2(EV.CHANGE, fullPath, { curr, prev });
|
|
45262
45364
|
});
|
|
@@ -45273,7 +45375,7 @@ var setFsWatchFileListener = (path2, fullPath, options, handlers) => {
|
|
|
45273
45375
|
delFromSet(cont, KEY_RAW, rawEmitter);
|
|
45274
45376
|
if (isEmptySet(cont.listeners)) {
|
|
45275
45377
|
FsWatchFileInstances.delete(fullPath);
|
|
45276
|
-
(0,
|
|
45378
|
+
(0, import_fs6.unwatchFile)(fullPath);
|
|
45277
45379
|
cont.options = cont.watcher = void 0;
|
|
45278
45380
|
Object.freeze(cont);
|
|
45279
45381
|
}
|
|
@@ -46116,7 +46218,7 @@ var FSWatcher = class extends import_events.EventEmitter {
|
|
|
46116
46218
|
const now = /* @__PURE__ */ new Date();
|
|
46117
46219
|
const writes = this._pendingWrites;
|
|
46118
46220
|
function awaitWriteFinishFn(prevStat) {
|
|
46119
|
-
(0,
|
|
46221
|
+
(0, import_fs7.stat)(fullPath, (err, curStat) => {
|
|
46120
46222
|
if (err || !writes.has(path2)) {
|
|
46121
46223
|
if (err && err.code !== "ENOENT")
|
|
46122
46224
|
awfEmit(err);
|
|
@@ -46293,13 +46395,13 @@ var esm_default = { watch, FSWatcher };
|
|
|
46293
46395
|
var import_scanner2 = __toESM(require_dist());
|
|
46294
46396
|
var import_formatters2 = __toESM(require_formatters());
|
|
46295
46397
|
function isScannableFile2(filePath) {
|
|
46296
|
-
const ext2 = (0,
|
|
46297
|
-
const fileName = (0,
|
|
46398
|
+
const ext2 = (0, import_path5.extname)(filePath).toLowerCase();
|
|
46399
|
+
const fileName = (0, import_path5.basename)(filePath);
|
|
46298
46400
|
if (import_scanner2.SPECIAL_FILES.includes(fileName)) return true;
|
|
46299
46401
|
return import_scanner2.SCANNABLE_EXTENSIONS.includes(ext2);
|
|
46300
46402
|
}
|
|
46301
46403
|
function getLanguage2(filePath) {
|
|
46302
|
-
const ext2 = (0,
|
|
46404
|
+
const ext2 = (0, import_path5.extname)(filePath).toLowerCase();
|
|
46303
46405
|
const langMap = {
|
|
46304
46406
|
".js": "javascript",
|
|
46305
46407
|
".jsx": "javascript",
|
|
@@ -46318,11 +46420,11 @@ function getLanguage2(filePath) {
|
|
|
46318
46420
|
}
|
|
46319
46421
|
function readScanFile(filePath) {
|
|
46320
46422
|
try {
|
|
46321
|
-
const stats = (0,
|
|
46423
|
+
const stats = (0, import_fs8.statSync)(filePath);
|
|
46322
46424
|
if (stats.size > import_scanner2.MAX_FILE_SIZE) return null;
|
|
46323
|
-
const content = (0,
|
|
46425
|
+
const content = (0, import_fs8.readFileSync)(filePath, "utf-8");
|
|
46324
46426
|
return {
|
|
46325
|
-
path: (0,
|
|
46427
|
+
path: (0, import_path5.relative)(process.cwd(), filePath),
|
|
46326
46428
|
content,
|
|
46327
46429
|
language: getLanguage2(filePath),
|
|
46328
46430
|
size: content.length
|
|
@@ -46339,7 +46441,7 @@ function debounce(fn, delay) {
|
|
|
46339
46441
|
};
|
|
46340
46442
|
}
|
|
46341
46443
|
async function watch2(targetPath, options) {
|
|
46342
|
-
const absolutePath = (0,
|
|
46444
|
+
const absolutePath = (0, import_path5.resolve)(targetPath);
|
|
46343
46445
|
const config = getConfig();
|
|
46344
46446
|
if ((options.depth === "validated" || options.depth === "deep") && !isAuthenticated()) {
|
|
46345
46447
|
if (!options.quiet) {
|
|
@@ -46349,31 +46451,40 @@ async function watch2(targetPath, options) {
|
|
|
46349
46451
|
}
|
|
46350
46452
|
options.depth = "cheap";
|
|
46351
46453
|
}
|
|
46352
|
-
|
|
46454
|
+
const changedFiles = /* @__PURE__ */ new Set();
|
|
46455
|
+
let isPaused = false;
|
|
46456
|
+
let scanCount = 0;
|
|
46457
|
+
let totalIssues = 0;
|
|
46458
|
+
const showBanner = () => {
|
|
46459
|
+
if (options.quiet) return;
|
|
46353
46460
|
console.log("");
|
|
46354
46461
|
console.log(source_default.bold(" \u{1F441}\uFE0F Oculum Watch Mode"));
|
|
46355
|
-
console.log(source_default.dim(" " + "\u2500".repeat(
|
|
46462
|
+
console.log(source_default.dim(" " + "\u2500".repeat(50)));
|
|
46356
46463
|
console.log("");
|
|
46357
46464
|
console.log(source_default.dim(" Watching: ") + source_default.white(absolutePath));
|
|
46358
46465
|
console.log(source_default.dim(" Depth: ") + source_default.white(options.depth === "cheap" ? "Quick (pattern matching)" : options.depth));
|
|
46359
|
-
console.log(source_default.dim("
|
|
46466
|
+
console.log(source_default.dim(" Status: ") + (isPaused ? source_default.yellow("Paused") : source_default.green("Active")));
|
|
46467
|
+
if (scanCount > 0) {
|
|
46468
|
+
console.log(source_default.dim(" Scans: ") + source_default.white(`${scanCount} (${totalIssues} issues found)`));
|
|
46469
|
+
}
|
|
46360
46470
|
console.log("");
|
|
46361
|
-
console.log(source_default.dim("
|
|
46471
|
+
console.log(source_default.dim(" Keyboard: ") + source_default.white("[r]") + source_default.dim(" rescan ") + source_default.white("[c]") + source_default.dim(" clear ") + source_default.white("[p]") + source_default.dim(" pause ") + source_default.white("[q]") + source_default.dim(" quit"));
|
|
46472
|
+
console.log(source_default.dim(" " + "\u2500".repeat(50)));
|
|
46362
46473
|
console.log("");
|
|
46363
|
-
}
|
|
46364
|
-
|
|
46474
|
+
};
|
|
46475
|
+
showBanner();
|
|
46365
46476
|
let isScanning = false;
|
|
46366
46477
|
const runScanOnChanges = debounce(async () => {
|
|
46367
|
-
if (isScanning || changedFiles.size === 0) return;
|
|
46478
|
+
if (isScanning || changedFiles.size === 0 || isPaused) return;
|
|
46368
46479
|
isScanning = true;
|
|
46369
46480
|
const filesToScan = Array.from(changedFiles);
|
|
46370
46481
|
changedFiles.clear();
|
|
46371
46482
|
if (options.clearOnScan && !options.quiet) {
|
|
46372
46483
|
console.clear();
|
|
46484
|
+
showBanner();
|
|
46373
46485
|
}
|
|
46374
46486
|
if (!options.quiet) {
|
|
46375
46487
|
const fileText = filesToScan.length === 1 ? "file" : "files";
|
|
46376
|
-
console.log("");
|
|
46377
46488
|
console.log(source_default.cyan(` \u27F3 [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Scanning ${filesToScan.length} changed ${fileText}...`));
|
|
46378
46489
|
}
|
|
46379
46490
|
const scanFiles = [];
|
|
@@ -46383,7 +46494,7 @@ async function watch2(targetPath, options) {
|
|
|
46383
46494
|
}
|
|
46384
46495
|
if (scanFiles.length === 0) {
|
|
46385
46496
|
if (!options.quiet) {
|
|
46386
|
-
console.log(source_default.dim("No scannable files found."));
|
|
46497
|
+
console.log(source_default.dim(" No scannable files found."));
|
|
46387
46498
|
}
|
|
46388
46499
|
isScanning = false;
|
|
46389
46500
|
return;
|
|
@@ -46392,24 +46503,36 @@ async function watch2(targetPath, options) {
|
|
|
46392
46503
|
const result = await (0, import_scanner2.runScan)(
|
|
46393
46504
|
scanFiles,
|
|
46394
46505
|
{
|
|
46395
|
-
name: (0,
|
|
46506
|
+
name: (0, import_path5.basename)(absolutePath),
|
|
46396
46507
|
url: "",
|
|
46397
46508
|
branch: "watch"
|
|
46398
46509
|
},
|
|
46399
46510
|
{
|
|
46400
46511
|
enableAI: options.depth !== "cheap" && isAuthenticated(),
|
|
46401
46512
|
scanDepth: options.depth,
|
|
46402
|
-
scanMode: "incremental"
|
|
46513
|
+
scanMode: "incremental",
|
|
46514
|
+
quiet: true
|
|
46515
|
+
// Suppress internal scanner logs
|
|
46403
46516
|
}
|
|
46404
46517
|
);
|
|
46518
|
+
scanCount++;
|
|
46519
|
+
totalIssues += result.vulnerabilities.length;
|
|
46520
|
+
try {
|
|
46521
|
+
addScanHistoryEntry({
|
|
46522
|
+
targetPath: absolutePath,
|
|
46523
|
+
options: { depth: options.depth, format: "terminal", failOn: "high" },
|
|
46524
|
+
result
|
|
46525
|
+
});
|
|
46526
|
+
} catch {
|
|
46527
|
+
}
|
|
46405
46528
|
if (result.vulnerabilities.length === 0) {
|
|
46406
46529
|
if (!options.quiet) {
|
|
46407
|
-
console.log(source_default.green("
|
|
46530
|
+
console.log(source_default.green(" \u2713 No issues found"));
|
|
46408
46531
|
}
|
|
46409
46532
|
} else {
|
|
46410
46533
|
const issueCount = result.vulnerabilities.length;
|
|
46411
46534
|
const issueText = issueCount === 1 ? "issue" : "issues";
|
|
46412
|
-
console.log(source_default.yellow(`
|
|
46535
|
+
console.log(source_default.yellow(` \u26A0 Found ${issueCount} ${issueText}:`));
|
|
46413
46536
|
console.log((0, import_formatters2.formatTerminalOutput)(result, {
|
|
46414
46537
|
maxFindingsPerGroup: 5
|
|
46415
46538
|
}));
|
|
@@ -46420,6 +46543,47 @@ async function watch2(targetPath, options) {
|
|
|
46420
46543
|
}
|
|
46421
46544
|
isScanning = false;
|
|
46422
46545
|
}, options.debounce);
|
|
46546
|
+
const triggerFullRescan = async () => {
|
|
46547
|
+
if (isScanning) return;
|
|
46548
|
+
if (!options.quiet) {
|
|
46549
|
+
console.log(source_default.cyan(`
|
|
46550
|
+
\u27F3 [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Triggering full rescan...`));
|
|
46551
|
+
}
|
|
46552
|
+
const { glob: glob3 } = await Promise.resolve().then(() => (init_esm7(), esm_exports));
|
|
46553
|
+
const patterns2 = [
|
|
46554
|
+
"**/*.js",
|
|
46555
|
+
"**/*.jsx",
|
|
46556
|
+
"**/*.ts",
|
|
46557
|
+
"**/*.tsx",
|
|
46558
|
+
"**/*.py",
|
|
46559
|
+
"**/*.go",
|
|
46560
|
+
"**/*.java",
|
|
46561
|
+
"**/*.rb",
|
|
46562
|
+
"**/*.php",
|
|
46563
|
+
"**/*.cs",
|
|
46564
|
+
"**/package.json"
|
|
46565
|
+
];
|
|
46566
|
+
const ignorePatterns2 = [
|
|
46567
|
+
"**/node_modules/**",
|
|
46568
|
+
"**/dist/**",
|
|
46569
|
+
"**/build/**",
|
|
46570
|
+
"**/.git/**",
|
|
46571
|
+
"**/vendor/**",
|
|
46572
|
+
"**/__pycache__/**"
|
|
46573
|
+
];
|
|
46574
|
+
const allFiles2 = await glob3(patterns2, {
|
|
46575
|
+
cwd: absolutePath,
|
|
46576
|
+
ignore: ignorePatterns2,
|
|
46577
|
+
nodir: true,
|
|
46578
|
+
absolute: true
|
|
46579
|
+
});
|
|
46580
|
+
changedFiles.clear();
|
|
46581
|
+
for (const filePath of allFiles2) {
|
|
46582
|
+
changedFiles.add(filePath);
|
|
46583
|
+
}
|
|
46584
|
+
isScanning = false;
|
|
46585
|
+
runScanOnChanges();
|
|
46586
|
+
};
|
|
46423
46587
|
const watcher = esm_default.watch(absolutePath, {
|
|
46424
46588
|
ignored: [
|
|
46425
46589
|
"**/node_modules/**",
|
|
@@ -46439,17 +46603,17 @@ async function watch2(targetPath, options) {
|
|
|
46439
46603
|
});
|
|
46440
46604
|
watcher.on("change", (filePath) => {
|
|
46441
46605
|
if (!isScannableFile2(filePath)) return;
|
|
46442
|
-
changedFiles.add((0,
|
|
46606
|
+
changedFiles.add((0, import_path5.resolve)(filePath));
|
|
46443
46607
|
if (!options.quiet) {
|
|
46444
|
-
console.log(source_default.dim(`Changed: ${(0,
|
|
46608
|
+
console.log(source_default.dim(`Changed: ${(0, import_path5.relative)(process.cwd(), filePath)}`));
|
|
46445
46609
|
}
|
|
46446
46610
|
runScanOnChanges();
|
|
46447
46611
|
});
|
|
46448
46612
|
watcher.on("add", (filePath) => {
|
|
46449
46613
|
if (!isScannableFile2(filePath)) return;
|
|
46450
|
-
changedFiles.add((0,
|
|
46614
|
+
changedFiles.add((0, import_path5.resolve)(filePath));
|
|
46451
46615
|
if (!options.quiet) {
|
|
46452
|
-
console.log(source_default.dim(`Added: ${(0,
|
|
46616
|
+
console.log(source_default.dim(`Added: ${(0, import_path5.relative)(process.cwd(), filePath)}`));
|
|
46453
46617
|
}
|
|
46454
46618
|
runScanOnChanges();
|
|
46455
46619
|
});
|
|
@@ -46457,13 +46621,47 @@ async function watch2(targetPath, options) {
|
|
|
46457
46621
|
const enhanced = enhanceError(error);
|
|
46458
46622
|
console.error(formatError(enhanced));
|
|
46459
46623
|
});
|
|
46460
|
-
|
|
46624
|
+
const cleanup = () => {
|
|
46461
46625
|
if (!options.quiet) {
|
|
46462
|
-
console.log(source_default.dim("\n\
|
|
46626
|
+
console.log(source_default.dim("\n\n Stopping watch mode..."));
|
|
46627
|
+
if (scanCount > 0) {
|
|
46628
|
+
console.log(source_default.dim(` Completed ${scanCount} scans, found ${totalIssues} total issues.`));
|
|
46629
|
+
}
|
|
46630
|
+
console.log("");
|
|
46631
|
+
}
|
|
46632
|
+
if (process.stdin.isTTY) {
|
|
46633
|
+
process.stdin.setRawMode(false);
|
|
46463
46634
|
}
|
|
46464
46635
|
watcher.close();
|
|
46465
46636
|
process.exit(0);
|
|
46466
|
-
}
|
|
46637
|
+
};
|
|
46638
|
+
if (process.stdin.isTTY && !options.quiet) {
|
|
46639
|
+
readline.emitKeypressEvents(process.stdin);
|
|
46640
|
+
process.stdin.setRawMode(true);
|
|
46641
|
+
process.stdin.on("keypress", (ch, key) => {
|
|
46642
|
+
if (!key) return;
|
|
46643
|
+
if (key.name === "r") {
|
|
46644
|
+
triggerFullRescan();
|
|
46645
|
+
} else if (key.name === "c") {
|
|
46646
|
+
console.clear();
|
|
46647
|
+
showBanner();
|
|
46648
|
+
} else if (key.name === "p") {
|
|
46649
|
+
isPaused = !isPaused;
|
|
46650
|
+
if (isPaused) {
|
|
46651
|
+
console.log(source_default.yellow("\n \u23F8 Watch paused. Press [p] to resume.\n"));
|
|
46652
|
+
} else {
|
|
46653
|
+
console.log(source_default.green("\n \u25B6 Watch resumed.\n"));
|
|
46654
|
+
if (changedFiles.size > 0) {
|
|
46655
|
+
runScanOnChanges();
|
|
46656
|
+
}
|
|
46657
|
+
}
|
|
46658
|
+
} else if (key.name === "q" || key.ctrl && key.name === "c") {
|
|
46659
|
+
cleanup();
|
|
46660
|
+
}
|
|
46661
|
+
});
|
|
46662
|
+
} else {
|
|
46663
|
+
process.on("SIGINT", cleanup);
|
|
46664
|
+
}
|
|
46467
46665
|
if (!options.quiet) {
|
|
46468
46666
|
console.log(source_default.dim("Performing initial scan..."));
|
|
46469
46667
|
}
|
|
@@ -46507,9 +46705,15 @@ Examples:
|
|
|
46507
46705
|
$ oculum watch . --clear # Clear console between scans
|
|
46508
46706
|
$ oculum watch . --debounce 1000 # Wait 1s after changes
|
|
46509
46707
|
|
|
46708
|
+
Keyboard Controls:
|
|
46709
|
+
[r] Rescan all files
|
|
46710
|
+
[c] Clear console
|
|
46711
|
+
[p] Pause/resume watching
|
|
46712
|
+
[q] Quit watch mode
|
|
46713
|
+
|
|
46510
46714
|
Tips:
|
|
46511
46715
|
\u2022 Watch mode uses 'cheap' depth by default for fast feedback
|
|
46512
|
-
\u2022
|
|
46716
|
+
\u2022 Scans are recorded to history (view with: oculum history)
|
|
46513
46717
|
\u2022 Combine with oculum.config.json for project-specific settings
|
|
46514
46718
|
`).action((path2, cliOptions) => {
|
|
46515
46719
|
const projectConfig = loadProjectConfig(path2, cliOptions.quiet);
|
|
@@ -47132,59 +47336,6 @@ var Y2 = ({ indicator: t = "dots" } = {}) => {
|
|
|
47132
47336
|
// src/commands/ui.ts
|
|
47133
47337
|
var import_fs11 = require("fs");
|
|
47134
47338
|
|
|
47135
|
-
// src/utils/history.ts
|
|
47136
|
-
var import_fs8 = require("fs");
|
|
47137
|
-
var import_path5 = require("path");
|
|
47138
|
-
var import_os3 = require("os");
|
|
47139
|
-
var import_crypto = require("crypto");
|
|
47140
|
-
var CONFIG_DIR2 = (0, import_path5.join)((0, import_os3.homedir)(), ".oculum");
|
|
47141
|
-
var HISTORY_FILE = (0, import_path5.join)(CONFIG_DIR2, "history.json");
|
|
47142
|
-
var MAX_ENTRIES = 25;
|
|
47143
|
-
function ensureConfigDir2() {
|
|
47144
|
-
if (!(0, import_fs8.existsSync)(CONFIG_DIR2)) {
|
|
47145
|
-
(0, import_fs8.mkdirSync)(CONFIG_DIR2, { recursive: true });
|
|
47146
|
-
}
|
|
47147
|
-
}
|
|
47148
|
-
function readHistoryFile() {
|
|
47149
|
-
try {
|
|
47150
|
-
if (!(0, import_fs8.existsSync)(HISTORY_FILE)) return [];
|
|
47151
|
-
const content = (0, import_fs8.readFileSync)(HISTORY_FILE, "utf-8");
|
|
47152
|
-
const parsed = JSON.parse(content);
|
|
47153
|
-
if (!Array.isArray(parsed)) return [];
|
|
47154
|
-
return parsed;
|
|
47155
|
-
} catch {
|
|
47156
|
-
return [];
|
|
47157
|
-
}
|
|
47158
|
-
}
|
|
47159
|
-
function writeHistoryFile(entries) {
|
|
47160
|
-
ensureConfigDir2();
|
|
47161
|
-
(0, import_fs8.writeFileSync)(HISTORY_FILE, JSON.stringify(entries, null, 2));
|
|
47162
|
-
}
|
|
47163
|
-
function listScanHistory() {
|
|
47164
|
-
return readHistoryFile();
|
|
47165
|
-
}
|
|
47166
|
-
function addScanHistoryEntry(input) {
|
|
47167
|
-
const entry = {
|
|
47168
|
-
id: input.id ?? (0, import_crypto.randomUUID)(),
|
|
47169
|
-
createdAt: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
47170
|
-
targetPath: input.targetPath,
|
|
47171
|
-
options: input.options,
|
|
47172
|
-
result: input.result
|
|
47173
|
-
};
|
|
47174
|
-
const current = readHistoryFile();
|
|
47175
|
-
const updated = [entry, ...current].slice(0, MAX_ENTRIES);
|
|
47176
|
-
writeHistoryFile(updated);
|
|
47177
|
-
return entry;
|
|
47178
|
-
}
|
|
47179
|
-
function deleteScanHistoryEntry(id) {
|
|
47180
|
-
const current = readHistoryFile();
|
|
47181
|
-
const updated = current.filter((e2) => e2.id !== id);
|
|
47182
|
-
writeHistoryFile(updated);
|
|
47183
|
-
}
|
|
47184
|
-
function clearScanHistory() {
|
|
47185
|
-
writeHistoryFile([]);
|
|
47186
|
-
}
|
|
47187
|
-
|
|
47188
47339
|
// src/utils/first-run.ts
|
|
47189
47340
|
var import_fs9 = require("fs");
|
|
47190
47341
|
var import_path6 = require("path");
|
|
@@ -48138,6 +48289,9 @@ async function runAuthFlow() {
|
|
|
48138
48289
|
const verified = await verifyApiKey(config.apiKey);
|
|
48139
48290
|
if (verified.valid && verified.tier) {
|
|
48140
48291
|
currentTier = verified.tier;
|
|
48292
|
+
if (currentTier !== config.tier || verified.email && verified.email !== config.email) {
|
|
48293
|
+
setAuthCredentials(config.apiKey, verified.email || config.email, currentTier);
|
|
48294
|
+
}
|
|
48141
48295
|
}
|
|
48142
48296
|
} catch {
|
|
48143
48297
|
}
|
|
@@ -48167,9 +48321,14 @@ async function runAuthFlow() {
|
|
|
48167
48321
|
s.stop("Stored credentials are invalid or expired.");
|
|
48168
48322
|
continue;
|
|
48169
48323
|
}
|
|
48324
|
+
const email = verified.email || getConfig().email || "unknown";
|
|
48325
|
+
const tier = verified.tier || "free";
|
|
48326
|
+
if (tier !== getConfig().tier || email !== getConfig().email) {
|
|
48327
|
+
setAuthCredentials(getConfig().apiKey, email, tier);
|
|
48328
|
+
}
|
|
48170
48329
|
s.stop("Authenticated");
|
|
48171
|
-
M2.info(`Email: ${
|
|
48172
|
-
M2.info(`Tier: ${
|
|
48330
|
+
M2.info(`Email: ${email}`);
|
|
48331
|
+
M2.info(`Tier: ${tier}`);
|
|
48173
48332
|
} catch (err) {
|
|
48174
48333
|
s.stop("Verification failed");
|
|
48175
48334
|
M2.error(String(err));
|
|
@@ -48921,6 +49080,200 @@ function showTroubleshooting() {
|
|
|
48921
49080
|
console.log(source_default.white(" - Get support: ") + source_default.cyan("support@oculum.dev\n"));
|
|
48922
49081
|
}
|
|
48923
49082
|
|
|
49083
|
+
// src/commands/history.ts
|
|
49084
|
+
function formatDate3(isoString) {
|
|
49085
|
+
const date = new Date(isoString);
|
|
49086
|
+
return date.toLocaleString("en-US", {
|
|
49087
|
+
month: "short",
|
|
49088
|
+
day: "numeric",
|
|
49089
|
+
hour: "2-digit",
|
|
49090
|
+
minute: "2-digit"
|
|
49091
|
+
});
|
|
49092
|
+
}
|
|
49093
|
+
function truncate(str, maxLen) {
|
|
49094
|
+
if (str.length <= maxLen) return str;
|
|
49095
|
+
return str.slice(0, maxLen - 2) + "..";
|
|
49096
|
+
}
|
|
49097
|
+
function getStatusBadge(entry) {
|
|
49098
|
+
const { severityCounts } = entry.result;
|
|
49099
|
+
if (severityCounts.critical > 0 || severityCounts.high > 0) {
|
|
49100
|
+
return source_default.red("FAIL");
|
|
49101
|
+
}
|
|
49102
|
+
return source_default.green("PASS");
|
|
49103
|
+
}
|
|
49104
|
+
function getTotalIssues(entry) {
|
|
49105
|
+
const { severityCounts } = entry.result;
|
|
49106
|
+
return severityCounts.critical + severityCounts.high + severityCounts.medium + severityCounts.low + severityCounts.info;
|
|
49107
|
+
}
|
|
49108
|
+
function showHistoryList() {
|
|
49109
|
+
const entries = listScanHistory();
|
|
49110
|
+
if (entries.length === 0) {
|
|
49111
|
+
console.log(source_default.dim("\nNo scan history found."));
|
|
49112
|
+
console.log(source_default.dim("Run a scan with: oculum scan .\n"));
|
|
49113
|
+
return;
|
|
49114
|
+
}
|
|
49115
|
+
console.log(source_default.bold("\nRecent Scans"));
|
|
49116
|
+
console.log(source_default.dim("\u2500".repeat(70)));
|
|
49117
|
+
console.log();
|
|
49118
|
+
console.log(
|
|
49119
|
+
source_default.dim(" ID ") + source_default.dim("Date ") + source_default.dim("Path ") + source_default.dim("Issues ") + source_default.dim("Status")
|
|
49120
|
+
);
|
|
49121
|
+
console.log(source_default.dim(" " + "\u2500".repeat(66)));
|
|
49122
|
+
for (const entry of entries) {
|
|
49123
|
+
const id = entry.id.slice(0, 8);
|
|
49124
|
+
const date = formatDate3(entry.createdAt);
|
|
49125
|
+
const path2 = truncate(entry.targetPath, 22);
|
|
49126
|
+
const issues = getTotalIssues(entry);
|
|
49127
|
+
const status2 = getStatusBadge(entry);
|
|
49128
|
+
console.log(
|
|
49129
|
+
` ${source_default.cyan(id)} ${source_default.white(date.padEnd(16))} ${source_default.dim(path2.padEnd(22))} ${String(issues).padEnd(6)} ` + status2
|
|
49130
|
+
);
|
|
49131
|
+
}
|
|
49132
|
+
console.log();
|
|
49133
|
+
console.log(source_default.dim("\u2500".repeat(70)));
|
|
49134
|
+
console.log(source_default.dim("Run 'oculum history show <id>' for details"));
|
|
49135
|
+
console.log(source_default.dim("Run 'oculum history clear' to clear all history\n"));
|
|
49136
|
+
}
|
|
49137
|
+
function showScanDetails(id) {
|
|
49138
|
+
const entry = getScanHistoryEntry(id);
|
|
49139
|
+
if (!entry) {
|
|
49140
|
+
const entries = listScanHistory();
|
|
49141
|
+
const partial = entries.find((e2) => e2.id.startsWith(id));
|
|
49142
|
+
if (partial) {
|
|
49143
|
+
showScanDetailsForEntry(partial);
|
|
49144
|
+
return;
|
|
49145
|
+
}
|
|
49146
|
+
console.log(source_default.red(`
|
|
49147
|
+
Scan not found: ${id}`));
|
|
49148
|
+
console.log(source_default.dim("Run 'oculum history' to see available scans\n"));
|
|
49149
|
+
return;
|
|
49150
|
+
}
|
|
49151
|
+
showScanDetailsForEntry(entry);
|
|
49152
|
+
}
|
|
49153
|
+
function showScanDetailsForEntry(entry) {
|
|
49154
|
+
const { result, options, targetPath, createdAt, id } = entry;
|
|
49155
|
+
const { severityCounts, vulnerabilities, filesScanned, scanDuration } = result;
|
|
49156
|
+
console.log(source_default.bold("\nScan Details"));
|
|
49157
|
+
console.log(source_default.dim("\u2500".repeat(50)));
|
|
49158
|
+
console.log();
|
|
49159
|
+
console.log(source_default.white(" ID: ") + source_default.cyan(id.slice(0, 8)));
|
|
49160
|
+
console.log(source_default.white(" Date: ") + formatDate3(createdAt));
|
|
49161
|
+
console.log(source_default.white(" Path: ") + targetPath);
|
|
49162
|
+
console.log(source_default.white(" Depth: ") + options.depth);
|
|
49163
|
+
console.log(source_default.white(" Files: ") + filesScanned);
|
|
49164
|
+
console.log(source_default.white(" Duration: ") + `${(scanDuration / 1e3).toFixed(1)}s`);
|
|
49165
|
+
console.log();
|
|
49166
|
+
console.log(source_default.bold(" Findings"));
|
|
49167
|
+
console.log(source_default.dim(" " + "\u2500".repeat(30)));
|
|
49168
|
+
const severities = [
|
|
49169
|
+
{ name: "Critical", count: severityCounts.critical, color: source_default.red },
|
|
49170
|
+
{ name: "High", count: severityCounts.high, color: source_default.yellow },
|
|
49171
|
+
{ name: "Medium", count: severityCounts.medium, color: source_default.magenta },
|
|
49172
|
+
{ name: "Low", count: severityCounts.low, color: source_default.blue },
|
|
49173
|
+
{ name: "Info", count: severityCounts.info, color: source_default.gray }
|
|
49174
|
+
];
|
|
49175
|
+
for (const { name, count, color } of severities) {
|
|
49176
|
+
if (count > 0) {
|
|
49177
|
+
console.log(` ${color(name.padEnd(10))} ${count}`);
|
|
49178
|
+
}
|
|
49179
|
+
}
|
|
49180
|
+
const total = getTotalIssues(entry);
|
|
49181
|
+
if (total === 0) {
|
|
49182
|
+
console.log(source_default.green(" No issues found"));
|
|
49183
|
+
}
|
|
49184
|
+
if (vulnerabilities.length > 0) {
|
|
49185
|
+
console.log();
|
|
49186
|
+
console.log(source_default.bold(" Top Findings"));
|
|
49187
|
+
console.log(source_default.dim(" " + "\u2500".repeat(30)));
|
|
49188
|
+
const topFindings = vulnerabilities.slice(0, 5);
|
|
49189
|
+
for (const finding of topFindings) {
|
|
49190
|
+
const severityColor = finding.severity === "critical" ? source_default.red : finding.severity === "high" ? source_default.yellow : finding.severity === "medium" ? source_default.magenta : finding.severity === "low" ? source_default.blue : source_default.gray;
|
|
49191
|
+
console.log(
|
|
49192
|
+
` ${severityColor(finding.severity.toUpperCase().padEnd(8))} ` + source_default.white(truncate(finding.title, 40))
|
|
49193
|
+
);
|
|
49194
|
+
console.log(source_default.dim(` ${finding.filePath}:${finding.lineNumber}`));
|
|
49195
|
+
}
|
|
49196
|
+
if (vulnerabilities.length > 5) {
|
|
49197
|
+
console.log(source_default.dim(` ... and ${vulnerabilities.length - 5} more`));
|
|
49198
|
+
}
|
|
49199
|
+
}
|
|
49200
|
+
console.log();
|
|
49201
|
+
console.log(source_default.dim("\u2500".repeat(50)));
|
|
49202
|
+
console.log(source_default.dim(`Run 'oculum history delete ${id.slice(0, 8)}' to remove this scan
|
|
49203
|
+
`));
|
|
49204
|
+
}
|
|
49205
|
+
function clearHistory() {
|
|
49206
|
+
const entries = listScanHistory();
|
|
49207
|
+
if (entries.length === 0) {
|
|
49208
|
+
console.log(source_default.dim("\nNo history to clear.\n"));
|
|
49209
|
+
return;
|
|
49210
|
+
}
|
|
49211
|
+
clearScanHistory();
|
|
49212
|
+
console.log(source_default.green(`
|
|
49213
|
+
Cleared ${entries.length} scan entries.
|
|
49214
|
+
`));
|
|
49215
|
+
}
|
|
49216
|
+
function deleteEntry(id) {
|
|
49217
|
+
const entry = getScanHistoryEntry(id);
|
|
49218
|
+
if (!entry) {
|
|
49219
|
+
const entries = listScanHistory();
|
|
49220
|
+
const partial = entries.find((e2) => e2.id.startsWith(id));
|
|
49221
|
+
if (partial) {
|
|
49222
|
+
deleteScanHistoryEntry(partial.id);
|
|
49223
|
+
console.log(source_default.green(`
|
|
49224
|
+
Deleted scan ${partial.id.slice(0, 8)}
|
|
49225
|
+
`));
|
|
49226
|
+
return;
|
|
49227
|
+
}
|
|
49228
|
+
console.log(source_default.red(`
|
|
49229
|
+
Scan not found: ${id}
|
|
49230
|
+
`));
|
|
49231
|
+
return;
|
|
49232
|
+
}
|
|
49233
|
+
deleteScanHistoryEntry(entry.id);
|
|
49234
|
+
console.log(source_default.green(`
|
|
49235
|
+
Deleted scan ${entry.id.slice(0, 8)}
|
|
49236
|
+
`));
|
|
49237
|
+
}
|
|
49238
|
+
var historyCommand = new Command("history").description("View and manage scan history").argument("[subcommand]", "show, clear, or delete").argument("[id]", "scan ID for show/delete").action((subcommand, id) => {
|
|
49239
|
+
if (!subcommand) {
|
|
49240
|
+
showHistoryList();
|
|
49241
|
+
return;
|
|
49242
|
+
}
|
|
49243
|
+
switch (subcommand) {
|
|
49244
|
+
case "show":
|
|
49245
|
+
if (!id) {
|
|
49246
|
+
console.log(source_default.red("\nPlease provide a scan ID"));
|
|
49247
|
+
console.log(source_default.dim("Usage: oculum history show <id>\n"));
|
|
49248
|
+
return;
|
|
49249
|
+
}
|
|
49250
|
+
showScanDetails(id);
|
|
49251
|
+
break;
|
|
49252
|
+
case "clear":
|
|
49253
|
+
clearHistory();
|
|
49254
|
+
break;
|
|
49255
|
+
case "delete":
|
|
49256
|
+
if (!id) {
|
|
49257
|
+
console.log(source_default.red("\nPlease provide a scan ID"));
|
|
49258
|
+
console.log(source_default.dim("Usage: oculum history delete <id>\n"));
|
|
49259
|
+
return;
|
|
49260
|
+
}
|
|
49261
|
+
deleteEntry(id);
|
|
49262
|
+
break;
|
|
49263
|
+
default:
|
|
49264
|
+
showScanDetails(subcommand);
|
|
49265
|
+
}
|
|
49266
|
+
}).addHelpText("after", `
|
|
49267
|
+
Examples:
|
|
49268
|
+
$ oculum history List recent scans
|
|
49269
|
+
$ oculum history show abc123 View scan details
|
|
49270
|
+
$ oculum history abc123 Quick access (same as show)
|
|
49271
|
+
$ oculum history delete abc Delete a scan by ID
|
|
49272
|
+
$ oculum history clear Clear all history
|
|
49273
|
+
|
|
49274
|
+
Note: History stores up to 25 recent scans locally.
|
|
49275
|
+
`);
|
|
49276
|
+
|
|
48924
49277
|
// src/utils/ci-detect.ts
|
|
48925
49278
|
var CI_ENV_VARS = [
|
|
48926
49279
|
"CI",
|
|
@@ -48982,7 +49335,7 @@ function shouldRunUI() {
|
|
|
48982
49335
|
return isOcAlias || isUICommand;
|
|
48983
49336
|
}
|
|
48984
49337
|
var program2 = new Command();
|
|
48985
|
-
program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.
|
|
49338
|
+
program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.9").addHelpText("after", `
|
|
48986
49339
|
Quick Start:
|
|
48987
49340
|
$ oculum scan . Scan current directory (free)
|
|
48988
49341
|
$ oculum ui Interactive mode with guided setup
|
|
@@ -48991,6 +49344,7 @@ Quick Start:
|
|
|
48991
49344
|
Common Commands:
|
|
48992
49345
|
scan [path] Scan files for security vulnerabilities
|
|
48993
49346
|
watch [path] Watch files and scan on changes
|
|
49347
|
+
history View and manage scan history
|
|
48994
49348
|
ui Interactive terminal UI
|
|
48995
49349
|
login Authenticate with Oculum
|
|
48996
49350
|
status Check authentication status
|
|
@@ -49013,6 +49367,7 @@ program2.addCommand(usageCommand);
|
|
|
49013
49367
|
program2.addCommand(watchCommand);
|
|
49014
49368
|
program2.addCommand(uiCommand);
|
|
49015
49369
|
program2.addCommand(helpCommand);
|
|
49370
|
+
program2.addCommand(historyCommand);
|
|
49016
49371
|
async function main2() {
|
|
49017
49372
|
const interactive = isInteractiveTerminal();
|
|
49018
49373
|
if (interactive && shouldRunUI()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oculum/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "AI-native security scanner CLI for detecting vulnerabilities in AI-generated code, BYOK patterns, and modern web applications",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"url": "https://github.com/flexipie/oculum/issues"
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
|
-
"build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --banner:js=\"#!/usr/bin/env node\" --define:process.env.OCULUM_API_URL='undefined' --define:VERSION='\"1.0.
|
|
22
|
+
"build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --banner:js=\"#!/usr/bin/env node\" --define:process.env.OCULUM_API_URL='undefined' --define:VERSION='\"1.0.9\"'",
|
|
23
23
|
"dev": "npm run build -- --watch",
|
|
24
24
|
"test": "echo \"No tests configured yet\"",
|
|
25
25
|
"lint": "eslint src/"
|