git-push-time 1.1.2 → 1.1.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.
- package/.github/workflows/npm-publish.yml +59 -32
- package/README.md +1 -1
- package/bin/index.js +322 -339
- package/lib/commands/edit-time.js +141 -141
- package/lib/commands/fix.js +113 -113
- package/lib/commands/github.js +429 -445
- package/lib/commands/pull.js +90 -90
- package/lib/commands/push.js +720 -720
- package/lib/config.js +220 -220
- package/lib/fix.js +286 -286
- package/lib/git.js +525 -516
- package/lib/github.js +322 -322
- package/lib/secrets.js +92 -92
- package/lib/ui.js +1060 -1063
- package/lib/utils.js +120 -120
- package/package.json +36 -36
package/lib/secrets.js
CHANGED
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
import { spawnSync } from 'child_process';
|
|
2
|
-
|
|
3
|
-
// Pattern quét secret phổ biến. Giữ ở mức precision cao để tránh false-positive.
|
|
4
|
-
export const SECRET_PATTERNS = [
|
|
5
|
-
{ id: 'npm-token', label: 'npm Access Token', regex: /npm_[A-Za-z0-9]{8,}/ },
|
|
6
|
-
{ id: 'npm-authtoken', label: '_authToken trong .npmrc', regex: /_authToken\s*=\s*\S+/i },
|
|
7
|
-
{ id: 'github-token', label: 'GitHub Token (ghp_/gho_/github_pat_)', regex: /\b(ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{40,})\b/ },
|
|
8
|
-
{ id: 'aws-key', label: 'AWS Access Key (AKIA...)', regex: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
9
|
-
{ id: 'aws-secret', label: 'AWS Secret Key', regex: /aws_secret_access_key\s*=\s*\S+/i },
|
|
10
|
-
{ id: 'private-key', label: 'Private Key (PEM)', regex: /-----BEGIN (RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/ },
|
|
11
|
-
{ id: 'openai-key', label: 'OpenAI API Key', regex: /\bsk-(proj-)?[A-Za-z0-9_-]{20,}\b/ },
|
|
12
|
-
{ id: 'slack-token', label: 'Slack Token', regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
|
|
13
|
-
{ id: 'google-key', label: 'Google API Key', regex: /\bAIza[0-9A-Za-z_-]{30,}\b/ },
|
|
14
|
-
{ id: 'generic-password', label: 'Password gán cứng', regex: /\b(password|passwd|pwd)\s*[:=]\s*['"][^'"]{4,}['"]/i },
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
const SKIP_DIRS = ['node_modules/', '.git/', 'dist/', 'build/', '.next/'];
|
|
18
|
-
const SKIP_FILES = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
|
|
19
|
-
const MAX_FILE_BYTES = 512 * 1024; // bỏ qua file staged > 512KB
|
|
20
|
-
const MAX_MATCHES_PER_FILE = 3;
|
|
21
|
-
|
|
22
|
-
function listStagedFiles(cwd) {
|
|
23
|
-
try {
|
|
24
|
-
const r = spawnSync('git', ['diff', '--cached', '--name-only', '--diff-filter=ACM'], {
|
|
25
|
-
cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore']
|
|
26
|
-
});
|
|
27
|
-
if (r.status !== 0) return [];
|
|
28
|
-
return (r.stdout || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
29
|
-
} catch {
|
|
30
|
-
return [];
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function readStagedContent(file, cwd) {
|
|
35
|
-
try {
|
|
36
|
-
// Dùng spawnSync dạng mảng args (không qua shell) để tránh injection
|
|
37
|
-
// khi tên file chứa ký tự đặc biệt (", $, ;, &, |, ...).
|
|
38
|
-
const r = spawnSync('git', ['show', `:${file}`], {
|
|
39
|
-
cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: MAX_FILE_BYTES * 2
|
|
40
|
-
});
|
|
41
|
-
if (r.status !== 0) return null;
|
|
42
|
-
const content = r.stdout || '';
|
|
43
|
-
if (content.length > MAX_FILE_BYTES) return null; // file quá lớn, bỏ qua
|
|
44
|
-
if (content.includes('\0')) return null; // file binary
|
|
45
|
-
return content;
|
|
46
|
-
} catch {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function redactPreview(line) {
|
|
52
|
-
const s = line.trim().slice(0, 120);
|
|
53
|
-
// Che giữa secret, chỉ chừa 4 ký tự đầu/cuối
|
|
54
|
-
return s.replace(/([A-Za-z0-9_.-]{4})[A-Za-z0-9_.-]{6,}([A-Za-z0-9_.-]{2})?/g, '$1•••$2');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Quét nội dung các file đã stage để tìm secret.
|
|
59
|
-
* @param {string} cwd
|
|
60
|
-
* @returns {{ file: string, type: string, line: number, preview: string }[]}
|
|
61
|
-
*/
|
|
62
|
-
export function scanStagedContentForSecrets(cwd = process.cwd()) {
|
|
63
|
-
const findings = [];
|
|
64
|
-
const files = listStagedFiles(cwd).filter((f) => {
|
|
65
|
-
const normalized = String(f).replace(/\\/g, '/');
|
|
66
|
-
if (SKIP_DIRS.some((d) => normalized.startsWith(d) || normalized.includes('/' + d))) return false;
|
|
67
|
-
if (SKIP_FILES.includes(normalized.split('/').pop())) return false;
|
|
68
|
-
return true;
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
for (const file of files.slice(0, 100)) { // giới hạn 100 file để nhanh
|
|
72
|
-
const content = readStagedContent(file, cwd);
|
|
73
|
-
if (!content) continue;
|
|
74
|
-
const lines = content.split('\n');
|
|
75
|
-
let matchesInFile = 0;
|
|
76
|
-
for (let i = 0; i < lines.length; i++) {
|
|
77
|
-
const line = lines[i];
|
|
78
|
-
if (!line || line.length > 2000) continue;
|
|
79
|
-
// Bỏ qua comment ví dụ trong docs? vẫn báo để user tự quyết
|
|
80
|
-
for (const p of SECRET_PATTERNS) {
|
|
81
|
-
if (p.regex.test(line)) {
|
|
82
|
-
findings.push({ file, type: p.label, line: i + 1, preview: redactPreview(line) });
|
|
83
|
-
matchesInFile++;
|
|
84
|
-
break; // mỗi dòng chỉ báo 1 lần
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (matchesInFile >= MAX_MATCHES_PER_FILE) break;
|
|
88
|
-
if (findings.length >= 20) return findings; // chặn spam
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return findings;
|
|
92
|
-
}
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
// Pattern quét secret phổ biến. Giữ ở mức precision cao để tránh false-positive.
|
|
4
|
+
export const SECRET_PATTERNS = [
|
|
5
|
+
{ id: 'npm-token', label: 'npm Access Token', regex: /npm_[A-Za-z0-9]{8,}/ },
|
|
6
|
+
{ id: 'npm-authtoken', label: '_authToken trong .npmrc', regex: /_authToken\s*=\s*\S+/i },
|
|
7
|
+
{ id: 'github-token', label: 'GitHub Token (ghp_/gho_/github_pat_)', regex: /\b(ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{40,})\b/ },
|
|
8
|
+
{ id: 'aws-key', label: 'AWS Access Key (AKIA...)', regex: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
9
|
+
{ id: 'aws-secret', label: 'AWS Secret Key', regex: /aws_secret_access_key\s*=\s*\S+/i },
|
|
10
|
+
{ id: 'private-key', label: 'Private Key (PEM)', regex: /-----BEGIN (RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/ },
|
|
11
|
+
{ id: 'openai-key', label: 'OpenAI API Key', regex: /\bsk-(proj-)?[A-Za-z0-9_-]{20,}\b/ },
|
|
12
|
+
{ id: 'slack-token', label: 'Slack Token', regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
|
|
13
|
+
{ id: 'google-key', label: 'Google API Key', regex: /\bAIza[0-9A-Za-z_-]{30,}\b/ },
|
|
14
|
+
{ id: 'generic-password', label: 'Password gán cứng', regex: /\b(password|passwd|pwd)\s*[:=]\s*['"][^'"]{4,}['"]/i },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const SKIP_DIRS = ['node_modules/', '.git/', 'dist/', 'build/', '.next/'];
|
|
18
|
+
const SKIP_FILES = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
|
|
19
|
+
const MAX_FILE_BYTES = 512 * 1024; // bỏ qua file staged > 512KB
|
|
20
|
+
const MAX_MATCHES_PER_FILE = 3;
|
|
21
|
+
|
|
22
|
+
function listStagedFiles(cwd) {
|
|
23
|
+
try {
|
|
24
|
+
const r = spawnSync('git', ['diff', '--cached', '--name-only', '--diff-filter=ACM'], {
|
|
25
|
+
cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore']
|
|
26
|
+
});
|
|
27
|
+
if (r.status !== 0) return [];
|
|
28
|
+
return (r.stdout || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readStagedContent(file, cwd) {
|
|
35
|
+
try {
|
|
36
|
+
// Dùng spawnSync dạng mảng args (không qua shell) để tránh injection
|
|
37
|
+
// khi tên file chứa ký tự đặc biệt (", $, ;, &, |, ...).
|
|
38
|
+
const r = spawnSync('git', ['show', `:${file}`], {
|
|
39
|
+
cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: MAX_FILE_BYTES * 2
|
|
40
|
+
});
|
|
41
|
+
if (r.status !== 0) return null;
|
|
42
|
+
const content = r.stdout || '';
|
|
43
|
+
if (content.length > MAX_FILE_BYTES) return null; // file quá lớn, bỏ qua
|
|
44
|
+
if (content.includes('\0')) return null; // file binary
|
|
45
|
+
return content;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function redactPreview(line) {
|
|
52
|
+
const s = line.trim().slice(0, 120);
|
|
53
|
+
// Che giữa secret, chỉ chừa 4 ký tự đầu/cuối
|
|
54
|
+
return s.replace(/([A-Za-z0-9_.-]{4})[A-Za-z0-9_.-]{6,}([A-Za-z0-9_.-]{2})?/g, '$1•••$2');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Quét nội dung các file đã stage để tìm secret.
|
|
59
|
+
* @param {string} cwd
|
|
60
|
+
* @returns {{ file: string, type: string, line: number, preview: string }[]}
|
|
61
|
+
*/
|
|
62
|
+
export function scanStagedContentForSecrets(cwd = process.cwd()) {
|
|
63
|
+
const findings = [];
|
|
64
|
+
const files = listStagedFiles(cwd).filter((f) => {
|
|
65
|
+
const normalized = String(f).replace(/\\/g, '/');
|
|
66
|
+
if (SKIP_DIRS.some((d) => normalized.startsWith(d) || normalized.includes('/' + d))) return false;
|
|
67
|
+
if (SKIP_FILES.includes(normalized.split('/').pop())) return false;
|
|
68
|
+
return true;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
for (const file of files.slice(0, 100)) { // giới hạn 100 file để nhanh
|
|
72
|
+
const content = readStagedContent(file, cwd);
|
|
73
|
+
if (!content) continue;
|
|
74
|
+
const lines = content.split('\n');
|
|
75
|
+
let matchesInFile = 0;
|
|
76
|
+
for (let i = 0; i < lines.length; i++) {
|
|
77
|
+
const line = lines[i];
|
|
78
|
+
if (!line || line.length > 2000) continue;
|
|
79
|
+
// Bỏ qua comment ví dụ trong docs? vẫn báo để user tự quyết
|
|
80
|
+
for (const p of SECRET_PATTERNS) {
|
|
81
|
+
if (p.regex.test(line)) {
|
|
82
|
+
findings.push({ file, type: p.label, line: i + 1, preview: redactPreview(line) });
|
|
83
|
+
matchesInFile++;
|
|
84
|
+
break; // mỗi dòng chỉ báo 1 lần
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (matchesInFile >= MAX_MATCHES_PER_FILE) break;
|
|
88
|
+
if (findings.length >= 20) return findings; // chặn spam
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return findings;
|
|
92
|
+
}
|