yeknal 1.1.4 → 1.1.6

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.
Files changed (2) hide show
  1. package/bin/yeknal.js +249 -5
  2. package/package.json +1 -1
package/bin/yeknal.js CHANGED
@@ -579,9 +579,8 @@ async function checkSecretsAndEnv(projectDir, fileContents) {
579
579
  ));
580
580
  } else {
581
581
  checks.push(checkResult(
582
- ".gitignore exists", "Phase 10", 3, 0, "fail",
583
- "No .gitignore file found. Create one to prevent committing secrets, node_modules, build artifacts, and IDE files.",
584
- [{ file: ".gitignore", message: "Missing .gitignore file" }],
582
+ ".gitignore exists", "Phase 10", 3, 2, "warn",
583
+ ".gitignore not found one will be created after this scan completes.",
585
584
  ));
586
585
  }
587
586
 
@@ -1314,8 +1313,8 @@ async function checkDatabase(projectDir, fileContents) {
1314
1313
  ));
1315
1314
  } else {
1316
1315
  checks.push(checkResult(
1317
- "ORM or parameterized queries", "Phase 8", 5, 0, "warn",
1318
- "No ORM detected. Ensure parameterized queries are used for all database operations.",
1316
+ "ORM or parameterized queries", "Phase 8", 5, 0, "skip",
1317
+ "No ORM or DB library detected skipping parameterized query check.",
1319
1318
  ));
1320
1319
  }
1321
1320
 
@@ -1405,6 +1404,249 @@ async function checkDatabase(projectDir, fileContents) {
1405
1404
  return { name: "Database", checks };
1406
1405
  }
1407
1406
 
1407
+ // ---- Category 7: Frontend Code Security (15 pts) ----
1408
+ // security-best-practices references: javascript-general, react, vue, next.js
1409
+ async function checkFrontendSecurity(projectDir, fileContents) {
1410
+ const checks = [];
1411
+
1412
+ const jsTsFiles = [...fileContents.entries()].filter(([fp]) => {
1413
+ const ext = path.extname(fp).toLowerCase();
1414
+ return [".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs", ".vue"].includes(ext);
1415
+ });
1416
+
1417
+ if (jsTsFiles.length === 0) {
1418
+ checks.push(checkResult("No dynamic code execution", "security-best-practices/js-general", 5, 0, "skip", "No JS/TS files found"));
1419
+ checks.push(checkResult("No unsafe HTML injection sinks", "security-best-practices/react-vue", 5, 0, "skip", "No JS/TS files found"));
1420
+ checks.push(checkResult("No client-side secret exposure", "security-best-practices/next-react-vue", 5, 0, "skip", "No JS/TS files found"));
1421
+ return { name: "Frontend Security", checks };
1422
+ }
1423
+
1424
+ // Check: No eval() / new Function() with dynamic args (5 pts)
1425
+ const evalIssues = [];
1426
+ for (const [filePath, content] of jsTsFiles) {
1427
+ // Exclude test files and comments
1428
+ const lines = content.split("\n");
1429
+ lines.forEach((line, idx) => {
1430
+ const trimmed = line.trim();
1431
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) return;
1432
+ // eval(variable) — skip eval("literal string") as those are lower risk
1433
+ if (/\beval\s*\(\s*(?!['"`])/.test(line)) {
1434
+ evalIssues.push({ file: relPath(projectDir, filePath), line: idx + 1, message: "eval() with dynamic argument" });
1435
+ }
1436
+ // new Function(variable...) — skip new Function() with all literals
1437
+ if (/new\s+Function\s*\([^)]*(?:req\.|res\.|params|query|body|input|user|data)/.test(line)) {
1438
+ evalIssues.push({ file: relPath(projectDir, filePath), line: idx + 1, message: "new Function() with dynamic argument" });
1439
+ }
1440
+ // setTimeout/setInterval with a string argument containing a variable
1441
+ if (/(?:setTimeout|setInterval)\s*\(\s*(?!['"`])/.test(line) && !/(?:setTimeout|setInterval)\s*\(\s*(?:function|\()/.test(line)) {
1442
+ evalIssues.push({ file: relPath(projectDir, filePath), line: idx + 1, message: "setTimeout/setInterval with string argument" });
1443
+ }
1444
+ });
1445
+ }
1446
+
1447
+ if (evalIssues.length === 0) {
1448
+ checks.push(checkResult("No dynamic code execution", "security-best-practices/js-general", 5, 5, "pass", "No eval() or dynamic Function() usage detected"));
1449
+ } else {
1450
+ checks.push(checkResult(
1451
+ "No dynamic code execution", "security-best-practices/js-general", 5, 0, "fail",
1452
+ `${evalIssues.length} instance(s) of dynamic code execution (eval/Function/setTimeout). Refactor to static functions.`,
1453
+ evalIssues,
1454
+ ));
1455
+ }
1456
+
1457
+ // Check: No unsafe HTML injection sinks — dangerouslySetInnerHTML, v-html (5 pts)
1458
+ const htmlSinkIssues = [];
1459
+ const sanitizers = ["DOMPurify", "sanitizeHtml", "sanitize-html", "xss", "isomorphic-dompurify"];
1460
+
1461
+ for (const [filePath, content] of jsTsFiles) {
1462
+ const hasSanitizer = sanitizers.some((s) => content.includes(s));
1463
+ const lines = content.split("\n");
1464
+
1465
+ lines.forEach((line, idx) => {
1466
+ const trimmed = line.trim();
1467
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) return;
1468
+
1469
+ // React dangerouslySetInnerHTML
1470
+ if (/dangerouslySetInnerHTML/.test(line) && !hasSanitizer) {
1471
+ htmlSinkIssues.push({ file: relPath(projectDir, filePath), line: idx + 1, message: "dangerouslySetInnerHTML without sanitizer" });
1472
+ }
1473
+ // Vue v-html
1474
+ if (/v-html\s*=/.test(line) && !hasSanitizer) {
1475
+ htmlSinkIssues.push({ file: relPath(projectDir, filePath), line: idx + 1, message: "v-html without sanitizer (DOMPurify etc.)" });
1476
+ }
1477
+ // jQuery .html() with variable
1478
+ if (/\$\(.*\)\.html\s*\(\s*(?!['"`])/.test(line) || /\.html\s*\(\s*(?:req\.|res\.|params|query|body|user|data|input)/.test(line)) {
1479
+ htmlSinkIssues.push({ file: relPath(projectDir, filePath), line: idx + 1, message: "jQuery .html() with dynamic content" });
1480
+ }
1481
+ });
1482
+ }
1483
+
1484
+ if (htmlSinkIssues.length === 0) {
1485
+ checks.push(checkResult("No unsafe HTML injection sinks", "security-best-practices/react-vue", 5, 5, "pass", "No unsafe innerHTML/dangerouslySetInnerHTML/v-html sinks detected"));
1486
+ } else {
1487
+ checks.push(checkResult(
1488
+ "No unsafe HTML injection sinks", "security-best-practices/react-vue", 5, 0, "fail",
1489
+ `${htmlSinkIssues.length} unsafe HTML sink(s). Use a sanitizer like DOMPurify before injecting HTML.`,
1490
+ htmlSinkIssues,
1491
+ ));
1492
+ }
1493
+
1494
+ // Check: No client-side secret exposure via env vars (5 pts)
1495
+ // NEXT_PUBLIC_*, REACT_APP_*, VITE_* should never hold real secrets
1496
+ const clientSecretIssues = [];
1497
+ const clientSecretPattern = /(?:NEXT_PUBLIC_|REACT_APP_|VITE_)(?:SECRET|KEY|TOKEN|PASSWORD|PASS|PWD|API_KEY|PRIVATE|AUTH)\s*[=:]\s*['"][^'"]{8,}['"]/gi;
1498
+
1499
+ for (const [filePath, content] of fileContents) {
1500
+ const base = path.basename(filePath).toLowerCase();
1501
+ // Only check .env files and source files, skip .env.example/.env.sample
1502
+ if (base.includes("example") || base.includes("sample") || base.includes("template")) continue;
1503
+
1504
+ let match;
1505
+ clientSecretPattern.lastIndex = 0;
1506
+ while ((match = clientSecretPattern.exec(content)) !== null) {
1507
+ const line = content.substring(0, match.index).split("\n").length;
1508
+ clientSecretIssues.push({ file: relPath(projectDir, filePath), line, message: `Client-exposed secret: ${match[0].substring(0, 40)}...` });
1509
+ }
1510
+ }
1511
+
1512
+ if (clientSecretIssues.length === 0) {
1513
+ checks.push(checkResult("No client-side secret exposure", "security-best-practices/next-react-vue", 5, 5, "pass", "No secrets found in client-exposed env vars (NEXT_PUBLIC_, REACT_APP_, VITE_)"));
1514
+ } else {
1515
+ checks.push(checkResult(
1516
+ "No client-side secret exposure", "security-best-practices/next-react-vue", 5, 0, "fail",
1517
+ `${clientSecretIssues.length} secret(s) exposed via client-side env vars. Move to server-only env vars.`,
1518
+ clientSecretIssues,
1519
+ ));
1520
+ }
1521
+
1522
+ return { name: "Frontend Security", checks };
1523
+ }
1524
+
1525
+ // ---- Category 8: Framework Configuration Security (13 pts) ----
1526
+ // security-best-practices references: django, flask, express, next.js
1527
+ async function checkFrameworkConfig(projectDir, fileContents) {
1528
+ const checks = [];
1529
+
1530
+ // Detect frameworks present
1531
+ const allContent = [...fileContents.values()].join("\n");
1532
+ const hasDjango = /(?:from django|import django|DJANGO_SETTINGS_MODULE|django\.conf)/i.test(allContent);
1533
+ const hasFlask = /(?:from flask|import flask|Flask\(__name__\))/i.test(allContent);
1534
+ const hasExpress = /(?:require\(['"]express['"]\)|from ['"]express['"])/i.test(allContent);
1535
+ const hasPython = [...fileContents.keys()].some((fp) => fp.endsWith(".py"));
1536
+ const hasJS = [...fileContents.keys()].some((fp) => [".js", ".ts", ".mjs"].includes(path.extname(fp)));
1537
+
1538
+ // Check: Django ALLOWED_HOSTS / DEBUG misconfiguration (5 pts)
1539
+ if (hasDjango) {
1540
+ const djangoIssues = [];
1541
+
1542
+ for (const [filePath, content] of fileContents) {
1543
+ if (!filePath.endsWith(".py")) continue;
1544
+ const lines = content.split("\n");
1545
+
1546
+ lines.forEach((line, idx) => {
1547
+ const trimmed = line.trim();
1548
+ if (trimmed.startsWith("#")) return;
1549
+ if (/ALLOWED_HOSTS\s*=\s*\[.*['"]\*['"]/.test(line)) {
1550
+ djangoIssues.push({ file: relPath(projectDir, filePath), line: idx + 1, message: "ALLOWED_HOSTS = ['*'] allows any host — set explicit domains in production" });
1551
+ }
1552
+ if (/^\s*DEBUG\s*=\s*True/.test(line) && !filePath.includes("test") && !filePath.includes("dev") && !filePath.includes("local")) {
1553
+ djangoIssues.push({ file: relPath(projectDir, filePath), line: idx + 1, message: "DEBUG = True — ensure this is not active in production" });
1554
+ }
1555
+ });
1556
+ }
1557
+
1558
+ if (djangoIssues.length === 0) {
1559
+ checks.push(checkResult("Django: safe ALLOWED_HOSTS / DEBUG", "security-best-practices/django", 5, 5, "pass", "No ALLOWED_HOSTS=['*'] or unconditional DEBUG=True detected"));
1560
+ } else {
1561
+ checks.push(checkResult(
1562
+ "Django: safe ALLOWED_HOSTS / DEBUG", "security-best-practices/django", 5, 0, "fail",
1563
+ `${djangoIssues.length} Django configuration issue(s). Wildcard hosts and DEBUG=True expose the application.`,
1564
+ djangoIssues,
1565
+ ));
1566
+ }
1567
+ } else if (hasPython) {
1568
+ checks.push(checkResult("Django: safe ALLOWED_HOSTS / DEBUG", "security-best-practices/django", 5, 0, "skip", "Django not detected"));
1569
+ } else {
1570
+ checks.push(checkResult("Django: safe ALLOWED_HOSTS / DEBUG", "security-best-practices/django", 5, 0, "skip", "No Python project detected"));
1571
+ }
1572
+
1573
+ // Check: No server-side template injection (SSTI) patterns (5 pts)
1574
+ const sstiIssues = [];
1575
+
1576
+ for (const [filePath, content] of fileContents) {
1577
+ const ext = path.extname(filePath).toLowerCase();
1578
+ if (![".py", ".js", ".ts", ".mjs"].includes(ext)) continue;
1579
+
1580
+ const lines = content.split("\n");
1581
+ lines.forEach((line, idx) => {
1582
+ const trimmed = line.trim();
1583
+ if (trimmed.startsWith("#") || trimmed.startsWith("//") || trimmed.startsWith("*")) return;
1584
+
1585
+ // Flask/Jinja2: render_template_string(request.* or user input)
1586
+ if (/render_template_string\s*\(.*(?:request\.|form\[|args\[|json\[|data\[)/.test(line)) {
1587
+ sstiIssues.push({ file: relPath(projectDir, filePath), line: idx + 1, message: "render_template_string() with user-controlled input (SSTI)" });
1588
+ }
1589
+ // Django: Template(user_input)
1590
+ if (/Template\s*\(.*(?:request\.|GET\[|POST\[|data\[)/.test(line)) {
1591
+ sstiIssues.push({ file: relPath(projectDir, filePath), line: idx + 1, message: "Django Template() with user-controlled input (SSTI)" });
1592
+ }
1593
+ // Express: res.render(req.query.* or req.body.*)
1594
+ if (/res\.render\s*\(\s*req\.(?:query|body|params)/.test(line)) {
1595
+ sstiIssues.push({ file: relPath(projectDir, filePath), line: idx + 1, message: "res.render() with user-controlled template name (SSTI)" });
1596
+ }
1597
+ });
1598
+ }
1599
+
1600
+ if (sstiIssues.length === 0) {
1601
+ checks.push(checkResult("No server-side template injection", "security-best-practices/flask-express", 5, 5, "pass", "No SSTI patterns (render_template_string/res.render with user input) detected"));
1602
+ } else {
1603
+ checks.push(checkResult(
1604
+ "No server-side template injection", "security-best-practices/flask-express", 5, 0, "fail",
1605
+ `${sstiIssues.length} SSTI risk(s). Never pass user-controlled strings directly to template engines.`,
1606
+ sstiIssues,
1607
+ ));
1608
+ }
1609
+
1610
+ // Check: No Express MemoryStore session (3 pts)
1611
+ if (hasExpress) {
1612
+ const memStoreIssues = [];
1613
+
1614
+ for (const [filePath, content] of fileContents) {
1615
+ const ext = path.extname(filePath).toLowerCase();
1616
+ if (![".js", ".ts", ".mjs", ".cjs"].includes(ext)) continue;
1617
+
1618
+ if (/express-session|session\s*\(/.test(content)) {
1619
+ // MemoryStore is the default when no store: option is set alongside session()
1620
+ if (!/store\s*:/.test(content) && /session\s*\(\s*\{/.test(content)) {
1621
+ const line = content.split("\n").findIndex((l) => /session\s*\(\s*\{/.test(l)) + 1;
1622
+ memStoreIssues.push({ file: relPath(projectDir, filePath), line, message: "express-session configured without explicit store — defaults to MemoryStore (not production-safe)" });
1623
+ }
1624
+ // Explicit new MemoryStore() is always wrong
1625
+ if (/new\s+MemoryStore\s*\(/.test(content)) {
1626
+ const line = content.split("\n").findIndex((l) => /new\s+MemoryStore/.test(l)) + 1;
1627
+ memStoreIssues.push({ file: relPath(projectDir, filePath), line, message: "Explicit MemoryStore — leaks memory and resets on restart. Use Redis or DB store." });
1628
+ }
1629
+ }
1630
+ }
1631
+
1632
+ if (memStoreIssues.length === 0) {
1633
+ checks.push(checkResult("Express: persistent session store", "security-best-practices/express", 3, 3, "pass", "No MemoryStore session configuration detected"));
1634
+ } else {
1635
+ checks.push(checkResult(
1636
+ "Express: persistent session store", "security-best-practices/express", 3, 0, "fail",
1637
+ `${memStoreIssues.length} Express session issue(s). Use connect-redis, connect-pg-simple, or similar persistent store.`,
1638
+ memStoreIssues,
1639
+ ));
1640
+ }
1641
+ } else if (hasJS) {
1642
+ checks.push(checkResult("Express: persistent session store", "security-best-practices/express", 3, 0, "skip", "Express not detected"));
1643
+ } else {
1644
+ checks.push(checkResult("Express: persistent session store", "security-best-practices/express", 3, 0, "skip", "No JS project detected"));
1645
+ }
1646
+
1647
+ return { name: "Framework Config", checks };
1648
+ }
1649
+
1408
1650
  // ---- Utility helpers for scanner ----
1409
1651
 
1410
1652
  function relPath(projectDir, filePath) {
@@ -1464,6 +1706,8 @@ async function runSecurityScan(projectDir) {
1464
1706
  await checkInputApi(projectDir, fileContents),
1465
1707
  await checkHeadersTransport(projectDir, fileContents),
1466
1708
  await checkDatabase(projectDir, fileContents),
1709
+ await checkFrontendSecurity(projectDir, fileContents),
1710
+ await checkFrameworkConfig(projectDir, fileContents),
1467
1711
  ];
1468
1712
 
1469
1713
  // Calculate scores
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yeknal",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "CLI to fetch markdown templates and sync AI agent skills",
5
5
  "main": "bin/yeknal.js",
6
6
  "bin": {