oh-my-customcode 0.13.2 → 0.13.3

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/dist/cli/index.js +374 -8
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -11852,6 +11852,30 @@ var en_default = {
11852
11852
  fail: "Some index.yaml files are invalid"
11853
11853
  }
11854
11854
  }
11855
+ },
11856
+ security: {
11857
+ description: "Scan for security issues in hooks, configs, and templates",
11858
+ verboseOption: "Show detailed scan results",
11859
+ scanning: "Running security scan...",
11860
+ passed: "Security scan passed! No issues found.",
11861
+ failed: "Security issues detected.",
11862
+ summary: "Security: {{pass}} passed, {{warn}} warnings, {{fail}} failed",
11863
+ checks: {
11864
+ hooks: {
11865
+ pass: "Hook scripts are safe",
11866
+ warn: "Hook scripts have potential concerns",
11867
+ fail: "Hook scripts contain dangerous patterns"
11868
+ },
11869
+ secrets: {
11870
+ pass: "No secrets found in configuration files",
11871
+ fail: "Secrets or credentials found in configuration"
11872
+ },
11873
+ integrity: {
11874
+ pass: "Template file permissions are secure",
11875
+ warn: "Some files have overly permissive settings",
11876
+ fail: "Security-sensitive files found in project"
11877
+ }
11878
+ }
11855
11879
  }
11856
11880
  },
11857
11881
  init: {
@@ -12144,6 +12168,30 @@ var ko_default = {
12144
12168
  fail: "일부 index.yaml 파일이 잘못됨"
12145
12169
  }
12146
12170
  }
12171
+ },
12172
+ security: {
12173
+ description: "훅, 설정, 템플릿의 보안 문제 검사",
12174
+ verboseOption: "상세 검사 결과 표시",
12175
+ scanning: "보안 검사 실행 중...",
12176
+ passed: "보안 검사 통과! 문제가 발견되지 않았습니다.",
12177
+ failed: "보안 문제가 발견되었습니다.",
12178
+ summary: "보안: {{pass}}개 통과, {{warn}}개 경고, {{fail}}개 실패",
12179
+ checks: {
12180
+ hooks: {
12181
+ pass: "훅 스크립트가 안전합니다",
12182
+ warn: "훅 스크립트에 잠재적 우려사항이 있습니다",
12183
+ fail: "훅 스크립트에 위험한 패턴이 포함되어 있습니다"
12184
+ },
12185
+ secrets: {
12186
+ pass: "설정 파일에 비밀 정보가 없습니다",
12187
+ fail: "설정 파일에서 비밀 정보 또는 인증 정보가 발견되었습니다"
12188
+ },
12189
+ integrity: {
12190
+ pass: "템플릿 파일 권한이 안전합니다",
12191
+ warn: "일부 파일의 권한이 과도하게 허용되어 있습니다",
12192
+ fail: "보안에 민감한 파일이 프로젝트에 존재합니다"
12193
+ }
12194
+ }
12147
12195
  }
12148
12196
  },
12149
12197
  init: {
@@ -14635,6 +14683,320 @@ async function listCommand(type = "all", options = {}) {
14635
14683
  }
14636
14684
  }
14637
14685
 
14686
+ // src/cli/security.ts
14687
+ import { constants as constants2, promises as fs2 } from "node:fs";
14688
+ import path2 from "node:path";
14689
+ async function pathExists2(targetPath) {
14690
+ try {
14691
+ await fs2.access(targetPath, constants2.F_OK);
14692
+ return true;
14693
+ } catch {
14694
+ return false;
14695
+ }
14696
+ }
14697
+ function isValidUtf8Text(content) {
14698
+ try {
14699
+ content.toString("utf-8");
14700
+ return true;
14701
+ } catch {
14702
+ return false;
14703
+ }
14704
+ }
14705
+ async function findAllFiles(dir2) {
14706
+ const results = [];
14707
+ try {
14708
+ const entries = await fs2.readdir(dir2, { withFileTypes: true });
14709
+ for (const entry of entries) {
14710
+ const fullPath = path2.join(dir2, entry.name);
14711
+ if (entry.isDirectory()) {
14712
+ const subResults = await findAllFiles(fullPath);
14713
+ results.push(...subResults);
14714
+ } else if (entry.isFile()) {
14715
+ results.push(fullPath);
14716
+ }
14717
+ }
14718
+ } catch {}
14719
+ return results;
14720
+ }
14721
+ var DANGEROUS_PATTERNS = [
14722
+ {
14723
+ pattern: /rm\s+-rf\s+[/~]/,
14724
+ name: "rm -rf with root/home path",
14725
+ severity: "fail"
14726
+ },
14727
+ {
14728
+ pattern: /curl\s+.*\|\s*(bash|sh|eval)/,
14729
+ name: "curl pipe to shell",
14730
+ severity: "fail"
14731
+ },
14732
+ {
14733
+ pattern: /wget\s+.*\|\s*(bash|sh|eval)/,
14734
+ name: "wget pipe to shell",
14735
+ severity: "fail"
14736
+ },
14737
+ { pattern: /\bsudo\b/, name: "sudo usage", severity: "warn" },
14738
+ { pattern: /chmod\s+777/, name: "chmod 777", severity: "warn" },
14739
+ { pattern: /\beval\s*\(/, name: "eval() usage", severity: "warn" },
14740
+ {
14741
+ pattern: /\$\{.*:-.*\}.*>\s*\/etc/,
14742
+ name: "write to /etc",
14743
+ severity: "fail"
14744
+ },
14745
+ {
14746
+ pattern: /base64\s+(-d|--decode).*\|\s*(bash|sh)/,
14747
+ name: "base64 decode to shell",
14748
+ severity: "fail"
14749
+ }
14750
+ ];
14751
+ function extractCommands(hooks) {
14752
+ const commands = [];
14753
+ if (!hooks || typeof hooks !== "object")
14754
+ return commands;
14755
+ for (const hookName in hooks) {
14756
+ const hook = hooks[hookName];
14757
+ if (hook && typeof hook === "object") {
14758
+ for (const eventName in hook) {
14759
+ const event = hook[eventName];
14760
+ if (Array.isArray(event)) {
14761
+ for (const item of event) {
14762
+ if (typeof item === "object" && item && "command" in item) {
14763
+ commands.push(String(item.command));
14764
+ }
14765
+ }
14766
+ }
14767
+ }
14768
+ }
14769
+ }
14770
+ return commands;
14771
+ }
14772
+ function scanCommands(commands) {
14773
+ const findings = [];
14774
+ let worstSeverity = "pass";
14775
+ for (const command of commands) {
14776
+ for (const { pattern, name, severity } of DANGEROUS_PATTERNS) {
14777
+ if (pattern.test(command)) {
14778
+ findings.push(`${name}: ${command.substring(0, 80)}${command.length > 80 ? "..." : ""}`);
14779
+ if (severity === "fail") {
14780
+ worstSeverity = "fail";
14781
+ } else if (severity === "warn" && worstSeverity === "pass") {
14782
+ worstSeverity = "warn";
14783
+ }
14784
+ }
14785
+ }
14786
+ }
14787
+ return { findings, worstSeverity };
14788
+ }
14789
+ async function checkHookScripts(targetDir, rootDir = ".claude") {
14790
+ const hooksFile = path2.join(targetDir, rootDir, "hooks", "hooks.json");
14791
+ const exists2 = await pathExists2(hooksFile);
14792
+ if (!exists2) {
14793
+ return {
14794
+ name: "Hook scripts",
14795
+ status: "pass",
14796
+ message: i18n.t("cli.security.checks.hooks.pass"),
14797
+ fixable: false
14798
+ };
14799
+ }
14800
+ try {
14801
+ const content = await fs2.readFile(hooksFile, "utf-8");
14802
+ const hooks = JSON.parse(content);
14803
+ const commands = extractCommands(hooks);
14804
+ const { findings, worstSeverity } = scanCommands(commands);
14805
+ if (findings.length > 0) {
14806
+ const message = worstSeverity === "fail" ? i18n.t("cli.security.checks.hooks.fail") : i18n.t("cli.security.checks.hooks.warn");
14807
+ return {
14808
+ name: "Hook scripts",
14809
+ status: worstSeverity,
14810
+ message: `${message} (${findings.length} issues)`,
14811
+ fixable: false,
14812
+ details: findings
14813
+ };
14814
+ }
14815
+ return {
14816
+ name: "Hook scripts",
14817
+ status: "pass",
14818
+ message: i18n.t("cli.security.checks.hooks.pass"),
14819
+ fixable: false
14820
+ };
14821
+ } catch (error2) {
14822
+ return {
14823
+ name: "Hook scripts",
14824
+ status: "warn",
14825
+ message: `Failed to parse hooks.json: ${error2 instanceof Error ? error2.message : String(error2)}`,
14826
+ fixable: false
14827
+ };
14828
+ }
14829
+ }
14830
+ async function checkConfigSecrets(targetDir, rootDir = ".claude") {
14831
+ const configDir = path2.join(targetDir, rootDir);
14832
+ const exists2 = await pathExists2(configDir);
14833
+ if (!exists2) {
14834
+ return {
14835
+ name: "Config secrets",
14836
+ status: "pass",
14837
+ message: i18n.t("cli.security.checks.secrets.pass"),
14838
+ fixable: false
14839
+ };
14840
+ }
14841
+ const SECRET_PATTERNS = [
14842
+ {
14843
+ pattern: /(?:AWS_SECRET|AWS_ACCESS_KEY|AWS_SESSION)[_A-Z]*\s*[=:]\s*['"]?[A-Za-z0-9/+=]{20,}/,
14844
+ name: "AWS credential"
14845
+ },
14846
+ {
14847
+ pattern: /(?:GITHUB_TOKEN|GH_TOKEN|GITHUB_PAT)\s*[=:]\s*['"]?(?:ghp_|gho_|ghs_|ghr_|github_pat_)[A-Za-z0-9_]+/,
14848
+ name: "GitHub token"
14849
+ },
14850
+ {
14851
+ pattern: /(?:sk-|sk_live_|sk_test_)[A-Za-z0-9]{20,}/,
14852
+ name: "API secret key (sk-*)"
14853
+ },
14854
+ {
14855
+ pattern: /(?:password|passwd|secret)\s*[=:]\s*['"]?[^\s'"]{8,}/i,
14856
+ name: "Hardcoded password/secret"
14857
+ },
14858
+ {
14859
+ pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/,
14860
+ name: "Private key"
14861
+ }
14862
+ ];
14863
+ const files = await findAllFiles(configDir);
14864
+ const findings = [];
14865
+ for (const file of files) {
14866
+ try {
14867
+ const content = await fs2.readFile(file);
14868
+ if (!isValidUtf8Text(content)) {
14869
+ continue;
14870
+ }
14871
+ const text = content.toString("utf-8");
14872
+ for (const { pattern, name } of SECRET_PATTERNS) {
14873
+ if (pattern.test(text)) {
14874
+ const relativePath = path2.relative(targetDir, file);
14875
+ findings.push(`${relativePath}: ${name}`);
14876
+ }
14877
+ }
14878
+ } catch {}
14879
+ }
14880
+ if (findings.length > 0) {
14881
+ return {
14882
+ name: "Config secrets",
14883
+ status: "fail",
14884
+ message: `${i18n.t("cli.security.checks.secrets.fail")} (${findings.length} found)`,
14885
+ fixable: false,
14886
+ details: findings
14887
+ };
14888
+ }
14889
+ return {
14890
+ name: "Config secrets",
14891
+ status: "pass",
14892
+ message: i18n.t("cli.security.checks.secrets.pass"),
14893
+ fixable: false
14894
+ };
14895
+ }
14896
+ async function checkEnvFiles(targetDir) {
14897
+ const findings = [];
14898
+ let severity = "pass";
14899
+ const envFiles = [".env", ".env.local", ".env.production", ".env.development"];
14900
+ for (const envFile of envFiles) {
14901
+ const envPath = path2.join(targetDir, envFile);
14902
+ if (await pathExists2(envPath)) {
14903
+ findings.push(`Security-sensitive file found: ${envFile}`);
14904
+ severity = "fail";
14905
+ }
14906
+ }
14907
+ return { findings, severity };
14908
+ }
14909
+ async function checkShellPermissions(targetDir, shellScripts) {
14910
+ const findings = [];
14911
+ let severity = "pass";
14912
+ for (const script of shellScripts) {
14913
+ try {
14914
+ const stats = await fs2.stat(script);
14915
+ const mode = stats.mode & 511;
14916
+ const relativePath = path2.relative(targetDir, script);
14917
+ if (mode === 511) {
14918
+ findings.push(`Overly permissive permissions (777): ${relativePath}`);
14919
+ if (severity === "pass") {
14920
+ severity = "warn";
14921
+ }
14922
+ } else if (mode & 2) {
14923
+ findings.push(`World-writable: ${relativePath}`);
14924
+ if (severity === "pass") {
14925
+ severity = "warn";
14926
+ }
14927
+ }
14928
+ } catch {}
14929
+ }
14930
+ return { findings, severity };
14931
+ }
14932
+ async function checkTemplateIntegrity(targetDir) {
14933
+ let worstSeverity = "pass";
14934
+ const allFindings = [];
14935
+ const envCheck = await checkEnvFiles(targetDir);
14936
+ allFindings.push(...envCheck.findings);
14937
+ if (envCheck.severity === "fail") {
14938
+ worstSeverity = "fail";
14939
+ }
14940
+ const allFiles = await findAllFiles(targetDir);
14941
+ const shellScripts = allFiles.filter((f) => f.endsWith(".sh"));
14942
+ const permCheck = await checkShellPermissions(targetDir, shellScripts);
14943
+ allFindings.push(...permCheck.findings);
14944
+ if (permCheck.severity === "warn" && worstSeverity === "pass") {
14945
+ worstSeverity = "warn";
14946
+ }
14947
+ if (allFindings.length > 0) {
14948
+ const message = worstSeverity === "fail" ? i18n.t("cli.security.checks.integrity.fail") : i18n.t("cli.security.checks.integrity.warn");
14949
+ return {
14950
+ name: "Template integrity",
14951
+ status: worstSeverity,
14952
+ message: `${message} (${allFindings.length} issues)`,
14953
+ fixable: false,
14954
+ details: allFindings
14955
+ };
14956
+ }
14957
+ return {
14958
+ name: "Template integrity",
14959
+ status: "pass",
14960
+ message: i18n.t("cli.security.checks.integrity.pass"),
14961
+ fixable: false
14962
+ };
14963
+ }
14964
+ async function securityCommand(_options = {}) {
14965
+ const targetDir = process.cwd();
14966
+ console.log(i18n.t("cli.security.scanning"));
14967
+ console.log("");
14968
+ const layout = getProviderLayout();
14969
+ const checks = await Promise.all([
14970
+ checkHookScripts(targetDir, layout.rootDir),
14971
+ checkConfigSecrets(targetDir, layout.rootDir),
14972
+ checkTemplateIntegrity(targetDir)
14973
+ ]);
14974
+ for (const check of checks) {
14975
+ printCheck(check);
14976
+ }
14977
+ const passCount = checks.filter((c) => c.status === "pass").length;
14978
+ const warnCount = checks.filter((c) => c.status === "warn").length;
14979
+ const failCount = checks.filter((c) => c.status === "fail").length;
14980
+ console.log("");
14981
+ if (failCount === 0 && warnCount === 0) {
14982
+ console.log(i18n.t("cli.security.passed"));
14983
+ } else {
14984
+ console.log(i18n.t("cli.security.failed"));
14985
+ }
14986
+ console.log(i18n.t("cli.security.summary", {
14987
+ pass: passCount,
14988
+ warn: warnCount,
14989
+ fail: failCount
14990
+ }));
14991
+ return {
14992
+ success: failCount === 0,
14993
+ checks,
14994
+ passCount,
14995
+ warnCount,
14996
+ failCount
14997
+ };
14998
+ }
14999
+
14638
15000
  // src/core/updater.ts
14639
15001
  import { join as join8 } from "node:path";
14640
15002
 
@@ -14807,11 +15169,11 @@ function getEntryTemplateName2(language) {
14807
15169
  return language === "ko" ? `${baseName}.md.ko` : `${baseName}.md.en`;
14808
15170
  }
14809
15171
  async function backupFile(filePath) {
14810
- const fs2 = await import("node:fs/promises");
15172
+ const fs3 = await import("node:fs/promises");
14811
15173
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
14812
15174
  const backupPath = `${filePath}.backup-${timestamp}`;
14813
15175
  if (await fileExists(filePath)) {
14814
- await fs2.copyFile(filePath, backupPath);
15176
+ await fs3.copyFile(filePath, backupPath);
14815
15177
  debug("update.file_backed_up", { path: filePath, backup: backupPath });
14816
15178
  }
14817
15179
  }
@@ -15023,8 +15385,8 @@ async function updateComponent(targetDir, component, customizations, options, co
15023
15385
  skipPaths.push(cc.path);
15024
15386
  }
15025
15387
  }
15026
- const path2 = await import("node:path");
15027
- const normalizedSkipPaths = skipPaths.map((p) => path2.relative(destPath, join8(targetDir, p)));
15388
+ const path3 = await import("node:path");
15389
+ const normalizedSkipPaths = skipPaths.map((p) => path3.relative(destPath, join8(targetDir, p)));
15028
15390
  await copyDirectory(srcPath, destPath, {
15029
15391
  overwrite: true,
15030
15392
  skipPaths: normalizedSkipPaths.length > 0 ? normalizedSkipPaths : undefined
@@ -15045,7 +15407,7 @@ function getComponentPath2(component) {
15045
15407
  async function backupInstallation(targetDir) {
15046
15408
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
15047
15409
  const backupDir = join8(targetDir, `.omcustom-backup-${timestamp}`);
15048
- const fs2 = await import("node:fs/promises");
15410
+ const fs3 = await import("node:fs/promises");
15049
15411
  await ensureDirectory(backupDir);
15050
15412
  const layout = getProviderLayout();
15051
15413
  const dirsToBackup = [layout.rootDir, "guides"];
@@ -15058,7 +15420,7 @@ async function backupInstallation(targetDir) {
15058
15420
  }
15059
15421
  const entryPath = join8(targetDir, layout.entryFile);
15060
15422
  if (await fileExists(entryPath)) {
15061
- await fs2.copyFile(entryPath, join8(backupDir, layout.entryFile));
15423
+ await fs3.copyFile(entryPath, join8(backupDir, layout.entryFile));
15062
15424
  }
15063
15425
  return backupDir;
15064
15426
  }
@@ -15131,8 +15493,8 @@ function printUpdateResults(result) {
15131
15493
  console.log(i18n.t("cli.update.preservedFiles", { count: result.preservedFiles.length }));
15132
15494
  }
15133
15495
  if (result.backedUpPaths.length > 0) {
15134
- for (const path2 of result.backedUpPaths) {
15135
- console.log(i18n.t("cli.update.backupCreated", { path: path2 }));
15496
+ for (const path3 of result.backedUpPaths) {
15497
+ console.log(i18n.t("cli.update.backupCreated", { path: path3 }));
15136
15498
  }
15137
15499
  }
15138
15500
  for (const warning of result.warnings) {
@@ -15169,6 +15531,10 @@ function createProgram() {
15169
15531
  program2.command("doctor").description(i18n.t("cli.doctor.description")).option("--fix", i18n.t("cli.doctor.fixOption")).action(async (options) => {
15170
15532
  await doctorCommand(options);
15171
15533
  });
15534
+ program2.command("security").description(i18n.t("cli.security.description")).option("--verbose", i18n.t("cli.security.verboseOption")).action(async (options) => {
15535
+ const result = await securityCommand(options);
15536
+ process.exitCode = result.success ? 0 : 1;
15537
+ });
15172
15538
  program2.hook("preAction", async (thisCommand, actionCommand) => {
15173
15539
  const opts = thisCommand.optsWithGlobals();
15174
15540
  const skipCheck = opts.skipVersionCheck || false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-customcode",
3
- "version": "0.13.2",
3
+ "version": "0.13.3",
4
4
  "description": "Batteries-included agent harness for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {