flagrix 0.1.1 → 0.1.2

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.
@@ -361,6 +361,91 @@ function collectEvidence(content, patterns, max = MAX_EVIDENCE_LINES) {
361
361
  return evidence;
362
362
  }
363
363
 
364
+ // node_modules/@flagrix/scanner-core/dist/utils/mask.js
365
+ var REGEX_ALLOWED_AFTER_CHAR = /* @__PURE__ */ new Set([..."=(:,[!&|?{};+*%^<>~"]);
366
+ var REGEX_ALLOWED_AFTER_WORD = /* @__PURE__ */ new Set(["return", "case", "typeof", "in", "of", "do", "void", "delete"]);
367
+ function maskRegexLiterals(content) {
368
+ return content.split("\n").map(maskLine).join("\n");
369
+ }
370
+ function maskLine(line) {
371
+ let out = "";
372
+ let i = 0;
373
+ while (i < line.length) {
374
+ const ch = line[i];
375
+ if (ch === '"' || ch === "'" || ch === "`") {
376
+ const end = stringEnd(line, i);
377
+ out += line.slice(i, end);
378
+ i = end;
379
+ continue;
380
+ }
381
+ if (ch === "/" && line[i + 1] === "/") {
382
+ out += line.slice(i);
383
+ break;
384
+ }
385
+ if (ch === "/" && regexCanStartAfter(out)) {
386
+ const end = regexEnd(line, i);
387
+ if (end !== -1) {
388
+ out += " ".repeat(end - i);
389
+ i = end;
390
+ continue;
391
+ }
392
+ }
393
+ out += ch;
394
+ i++;
395
+ }
396
+ return out;
397
+ }
398
+ function regexCanStartAfter(before) {
399
+ const trimmed = before.trimEnd();
400
+ if (trimmed === "")
401
+ return true;
402
+ if (REGEX_ALLOWED_AFTER_CHAR.has(trimmed.slice(-1)))
403
+ return true;
404
+ const word = trimmed.match(/[A-Za-z_$][A-Za-z0-9_$]*$/)?.[0];
405
+ return word !== void 0 && REGEX_ALLOWED_AFTER_WORD.has(word);
406
+ }
407
+ function regexEnd(line, start) {
408
+ let i = start + 1;
409
+ let inClass = false;
410
+ let body = 0;
411
+ for (; i < line.length; i++) {
412
+ const ch = line[i];
413
+ if (ch === "\\") {
414
+ i++;
415
+ body++;
416
+ continue;
417
+ }
418
+ if (inClass) {
419
+ if (ch === "]")
420
+ inClass = false;
421
+ } else if (ch === "[") {
422
+ inClass = true;
423
+ } else if (ch === "/") {
424
+ if (body === 0)
425
+ return -1;
426
+ let end = i + 1;
427
+ while (end < line.length && /[a-z]/i.test(line[end]))
428
+ end++;
429
+ return end;
430
+ }
431
+ body++;
432
+ }
433
+ return -1;
434
+ }
435
+ function stringEnd(line, start) {
436
+ const quote = line[start];
437
+ let i = start + 1;
438
+ for (; i < line.length; i++) {
439
+ if (line[i] === "\\") {
440
+ i++;
441
+ continue;
442
+ }
443
+ if (line[i] === quote)
444
+ return i + 1;
445
+ }
446
+ return line.length;
447
+ }
448
+
364
449
  // node_modules/@flagrix/scanner-core/dist/github/api-error.js
365
450
  async function githubApiError(response, fallback = "GitHub API error") {
366
451
  let apiMessage = "";
@@ -549,8 +634,9 @@ async function scanGitHubRepo(repo, options) {
549
634
  continue;
550
635
  const fileData = await contentResponse.json();
551
636
  const content = atob(fileData.content || "");
637
+ const matchable = /\.(?:js|ts)$/.test(file.path) ? maskRegexLiterals(content) : content;
552
638
  if (SCANNABLE_EXTENSIONS.some((ext) => file.path.endsWith(ext))) {
553
- fileContents.push({ path: file.path, content });
639
+ fileContents.push({ path: file.path, content: matchable });
554
640
  }
555
641
  if (file.path.endsWith("package.json")) {
556
642
  const pkgFindings = await scanPackageJson(content, maliciousPackages, file.path);
@@ -567,10 +653,10 @@ async function scanGitHubRepo(repo, options) {
567
653
  patternsMatched += pyFindings.length;
568
654
  }
569
655
  if (SCANNABLE_EXTENSIONS.some((ext) => file.path.endsWith(ext))) {
570
- const yaraFindings = applyYaraRules(content, yaraRules, file.path);
656
+ const yaraFindings = applyYaraRules(matchable, yaraRules, file.path);
571
657
  findings.push(...yaraFindings);
572
658
  patternsMatched += yaraFindings.length;
573
- const obfuscationFindings = detectObfuscation(content, file.path, loadedRuleIds);
659
+ const obfuscationFindings = detectObfuscation(matchable, file.path, loadedRuleIds);
574
660
  findings.push(...obfuscationFindings);
575
661
  patternsMatched += obfuscationFindings.length;
576
662
  }
@@ -612,7 +698,8 @@ async function scanGitHubRepo(repo, options) {
612
698
  patternsMatched += supplyChainFindings.length;
613
699
  }
614
700
  }
615
- const integrityFindings = await detectCodeIntegrityIssues(fileContents, repo);
701
+ const allPaths = tree.tree.filter((i) => i.type === "blob").map((i) => i.path);
702
+ const integrityFindings = await detectCodeIntegrityIssues(fileContents, allPaths, repo);
616
703
  findings.push(...integrityFindings);
617
704
  patternsMatched += integrityFindings.length;
618
705
  const riskScore = calculateRiskScore(findings);
@@ -1284,7 +1371,7 @@ function detectSuspiciousFileAccess(content, filePath, coveredRuleIds = /* @__PU
1284
1371
  }
1285
1372
  return findings;
1286
1373
  }
1287
- async function detectCodeIntegrityIssues(files, _repo) {
1374
+ async function detectCodeIntegrityIssues(files, allPaths, _repo) {
1288
1375
  const findings = [];
1289
1376
  for (const file of files) {
1290
1377
  if (file.path.endsWith(".min.js") || file.path.endsWith(".min.css"))
@@ -1306,8 +1393,9 @@ async function detectCodeIntegrityIssues(files, _repo) {
1306
1393
  }
1307
1394
  }
1308
1395
  }
1309
- const hasLicense = files.some((f) => /^LICENSE|^COPYING/i.test(f.path));
1310
- if (!hasLicense && files.length > 5) {
1396
+ const basename = (p) => p.split("/").pop() ?? p;
1397
+ const hasLicense = allPaths.some((p) => /^(LICENSE|COPYING)/i.test(basename(p)));
1398
+ if (!hasLicense && allPaths.length > 5) {
1311
1399
  findings.push({
1312
1400
  severity: "low",
1313
1401
  type: "CODE_INTEGRITY_ISSUE",
@@ -1315,8 +1403,8 @@ async function detectCodeIntegrityIssues(files, _repo) {
1315
1403
  codeExplanation: "\u{1F4C4} Missing license file. Legitimate open-source projects typically include a license. Absence may indicate a quickly assembled malicious repository."
1316
1404
  });
1317
1405
  }
1318
- const hasReadme = files.some((f) => /^README/i.test(f.path));
1319
- if (!hasReadme && files.length > 5) {
1406
+ const hasReadme = allPaths.some((p) => /^README/i.test(basename(p)));
1407
+ if (!hasReadme && allPaths.length > 5) {
1320
1408
  findings.push({
1321
1409
  severity: "low",
1322
1410
  type: "CODE_INTEGRITY_ISSUE",
package/dist/cli.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  scanGitHubRepo,
12
12
  scanGitHubUser,
13
13
  wantJson
14
- } from "./chunk-QKP4NQM3.js";
14
+ } from "./chunk-AS7DXMSG.js";
15
15
 
16
16
  // src/cli.ts
17
17
  import { parseArgs } from "util";
@@ -132,7 +132,7 @@ async function main() {
132
132
  }
133
133
  return runScanUser(target, { json: values.json, token: values.token });
134
134
  case "mcp": {
135
- const { runMcp } = await import("./mcp-B4RQXV5T.js");
135
+ const { runMcp } = await import("./mcp-LS7GRB2C.js");
136
136
  return runMcp();
137
137
  }
138
138
  default:
@@ -7,7 +7,7 @@ import {
7
7
  scanGitHubRepo,
8
8
  scanGitHubUser,
9
9
  securityScore
10
- } from "./chunk-QKP4NQM3.js";
10
+ } from "./chunk-AS7DXMSG.js";
11
11
 
12
12
  // src/commands/mcp.ts
13
13
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flagrix",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Scan GitHub repos and profiles for malware before you clone — CLI and MCP server for the Flagrix scanner",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -51,7 +51,7 @@
51
51
  "zod": "^3.24.0"
52
52
  },
53
53
  "devDependencies": {
54
- "@flagrix/scanner-core": "^0.1.0",
54
+ "@flagrix/scanner-core": "^0.1.2",
55
55
  "@types/node": "^20.0.0",
56
56
  "tsup": "^8.0.0",
57
57
  "typescript": "^5.0.0",