@vnptnan/gate 0.0.0-stage → 1.0.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.
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env bash
2
+ # Thư viện dùng chung của Cổng kiểm soát ATBMTT (VNPTNAN_GATE).
3
+ # Chạy được trên Git Bash (Windows), Linux (máy chủ GitLab) và macOS. Chỉ cần: bash >= 4, git, grep -E, awk, sed, wc, stat.
4
+ # Không có đầu vào/đầu ra riêng: gate-scan.sh và các hook gọi các hàm ở đây.
5
+
6
+ # ---------- Vị trí và cấu hình ----------
7
+ gate_root() {
8
+ local here
9
+ here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
10
+ echo "${GATE_ROOT:-$(dirname "$here")}"
11
+ }
12
+
13
+ GATE_ROOT=$(gate_root)
14
+ GATE_VERSION=$(cat "$GATE_ROOT/VERSION" 2>/dev/null || echo "0.0.0")
15
+ GATE_PREFIX="[VNPTNAN_GATE]"
16
+
17
+ # Giá trị mặc định; ghi đè bằng gate.conf ở gốc bộ công cụ, rồi .vnptnan-gate/gate.conf của dự án, rồi biến môi trường GATE_*.
18
+ GATE_MODE=${GATE_MODE:-} # enforce | audit
19
+ GATE_DEPS=${GATE_DEPS:-} # block | warn | off
20
+ GATE_DEPS_AUDIT=${GATE_DEPS_AUDIT:-} # 1 = chạy npm audit / dotnet list --vulnerable (cần mạng, chậm)
21
+ GATE_MAX_COMMITS=${GATE_MAX_COMMITS:-}
22
+ GATE_MAX_FILE=${GATE_MAX_FILE:-} # bytes; file lớn hơn không quét theo dòng
23
+ GATE_SKIP_PATHS=${GATE_SKIP_PATHS:-}
24
+ GATE_SKIP_EXTRA=${GATE_SKIP_EXTRA:-} # regex thêm vào SKIP_PATHS mặc định (khoá SKIP_PATHS_EXTRA trong gate.conf)
25
+ GATE_SERVER=${GATE_SERVER:-0} # 1 khi chạy trong pre-receive của GitLab
26
+ GATE_LOG=${GATE_LOG:-}
27
+ GATE_QUIET=${GATE_QUIET:-0}
28
+
29
+ gate_load_conf_file() {
30
+ local f=$1 key val
31
+ [ -f "$f" ] || return 0
32
+ while IFS='=' read -r key val || [ -n "$key" ]; do
33
+ key=$(printf '%s' "$key" | tr -d ' \r')
34
+ val=$(printf '%s' "$val" | sed 's/[[:space:]]*$//; s/\r$//')
35
+ case "$key" in
36
+ ''|'#'*) continue ;;
37
+ MODE) [ -z "$GATE_MODE" ] && GATE_MODE=$val ;;
38
+ DEPS) [ -z "$GATE_DEPS" ] && GATE_DEPS=$val ;;
39
+ DEPS_AUDIT) [ -z "$GATE_DEPS_AUDIT" ] && GATE_DEPS_AUDIT=$val ;;
40
+ MAX_COMMITS) [ -z "$GATE_MAX_COMMITS" ] && GATE_MAX_COMMITS=$val ;;
41
+ MAX_FILE) [ -z "$GATE_MAX_FILE" ] && GATE_MAX_FILE=$val ;;
42
+ SKIP_PATHS) [ -z "$GATE_SKIP_PATHS" ] && GATE_SKIP_PATHS=$val ;;
43
+ SKIP_PATHS_EXTRA) [ -z "$GATE_SKIP_EXTRA" ] && GATE_SKIP_EXTRA=$val ;;
44
+ LOG) [ -z "$GATE_LOG" ] && GATE_LOG=$val ;;
45
+ esac
46
+ done < "$f"
47
+ }
48
+
49
+ # Thứ tự ưu tiên: biến môi trường > .vnptnan-gate/gate.conf của dự án > gate.conf của bộ công cụ > mặc định.
50
+ gate_load_conf() {
51
+ local project_root=${1:-}
52
+ [ -n "$project_root" ] && gate_load_conf_file "$project_root/.vnptnan-gate/gate.conf"
53
+ gate_load_conf_file "$GATE_ROOT/gate.conf"
54
+ : "${GATE_MODE:=enforce}"
55
+ : "${GATE_DEPS:=warn}"
56
+ : "${GATE_DEPS_AUDIT:=0}"
57
+ : "${GATE_MAX_COMMITS:=500}"
58
+ : "${GATE_MAX_FILE:=5242880}"
59
+ : "${GATE_SKIP_PATHS:=(^|/)(node_modules|bin|obj|dist|build|out|packages|\.git|vendor|coverage|TestResults)/|\.(min\.js|min\.css|map|lock|snap)$|(^|/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|packages\.lock\.json)$|(^|/)\.vnptnan-gate/}"
60
+ [ -n "$GATE_SKIP_EXTRA" ] && GATE_SKIP_PATHS="$GATE_SKIP_PATHS|$GATE_SKIP_EXTRA"
61
+ if [ -z "$GATE_LOG" ]; then
62
+ if [ "$GATE_SERVER" = "1" ]; then GATE_LOG=/var/log/vnptnan-gate/gate.log
63
+ elif [ -n "${LOCALAPPDATA:-}" ]; then GATE_LOG="$LOCALAPPDATA/VNPTNAN_GATE/gate.log"
64
+ else GATE_LOG="$HOME/.vnptnan-gate/gate.log"; fi
65
+ fi
66
+ }
67
+
68
+ # ---------- Thông báo ----------
69
+ gate_say() {
70
+ # Mọi dòng ra stderr. Trong pre-receive, GitLab chỉ hiện dòng có tiền tố GL-HOOK-ERR:.
71
+ local line
72
+ for line in "$@"; do
73
+ if [ "$GATE_SERVER" = "1" ]; then printf 'GL-HOOK-ERR: %s %s\n' "$GATE_PREFIX" "$line" >&2
74
+ else printf '%s %s\n' "$GATE_PREFIX" "$line" >&2; fi
75
+ done
76
+ }
77
+
78
+ # ---------- Nạp rule ----------
79
+ # Mảng song song, chỉ số i: RULE_ID RULE_LEVEL RULE_KIND RULE_RE RULE_MSG
80
+ RULE_ID=(); RULE_LEVEL=(); RULE_KIND=(); RULE_RE=(); RULE_MSG=()
81
+ TEXT_BLOCK=''; TEXT_WARN=''; TEXT_AUDIT=''
82
+ # Ngoại lệ của dự án: ALLOW_ID ALLOW_PATHRE (từ .vnptnan-gate/allow.rules)
83
+ ALLOW_ID=(); ALLOW_PATHRE=()
84
+
85
+ gate_load_rules() {
86
+ local file=${1:-$GATE_ROOT/rules/gate.rules} line id level kind re msg
87
+ [ -f "$file" ] || { gate_say "Không tìm thấy file rule: $file"; return 2; }
88
+ while IFS= read -r line || [ -n "$line" ]; do
89
+ line=${line%$'\r'}
90
+ [ -z "$line" ] && continue
91
+ case "$line" in
92
+ '#'*) continue ;;
93
+ '@block-suffix'*) TEXT_BLOCK=${line#*$'\t'} ; continue ;;
94
+ '@warn-suffix'*) TEXT_WARN=${line#*$'\t'} ; continue ;;
95
+ '@audit-suffix'*) TEXT_AUDIT=${line#*$'\t'} ; continue ;;
96
+ '@'*) continue ;;
97
+ esac
98
+ IFS=$'\t' read -r id level kind re msg <<< "$line"
99
+ if [ -z "$id" ] || [ -z "$level" ] || [ -z "$kind" ] || [ -z "$re" ]; then
100
+ gate_say "Dòng rule hỏng (cần 5 cột ngăn bằng TAB): $line"; return 2
101
+ fi
102
+ case "$level" in block|warn) ;; *) gate_say "Mức không hợp lệ '$level' ở rule $id"; return 2 ;; esac
103
+ case "$kind" in line|path|count|size|allow|exempt) ;; *) gate_say "Loại không hợp lệ '$kind' ở rule $id"; return 2 ;; esac
104
+ RULE_ID+=("$id"); RULE_LEVEL+=("$level"); RULE_KIND+=("$kind"); RULE_RE+=("$re"); RULE_MSG+=("${msg:-}")
105
+ done < "$file"
106
+ }
107
+
108
+ gate_load_allow() {
109
+ # .vnptnan-gate/allow.rules trong repo: rule-id regex-đường-dẫn người-duyệt lý-do
110
+ local file=$1 line id pathre
111
+ [ -f "$file" ] || return 0
112
+ while IFS= read -r line || [ -n "$line" ]; do
113
+ line=${line%$'\r'}
114
+ [ -z "$line" ] && continue
115
+ case "$line" in '#'*) continue ;; esac
116
+ IFS=$'\t' read -r id pathre _ <<< "$line"
117
+ [ -n "$id" ] && [ -n "$pathre" ] || continue
118
+ ALLOW_ID+=("$id"); ALLOW_PATHRE+=("$pathre")
119
+ done < "$file"
120
+ }
121
+
122
+ gate_is_allowed() {
123
+ # $1 rule id, $2 path. Đúng khi dự án đã khai ngoại lệ cho rule đó trên đường dẫn đó.
124
+ local i
125
+ for i in "${!ALLOW_ID[@]}"; do
126
+ if [ "${ALLOW_ID[$i]}" = "$1" ] && printf '%s' "$2" | grep -qiE -- "${ALLOW_PATHRE[$i]}"; then return 0; fi
127
+ done
128
+ return 1
129
+ }
130
+
131
+ gate_path_skipped() { printf '%s' "$1" | grep -qiE -- "$GATE_SKIP_PATHS"; }
132
+
133
+ gate_path_exempt() {
134
+ local i
135
+ for i in "${!RULE_ID[@]}"; do
136
+ [ "${RULE_KIND[$i]}" = exempt ] || continue
137
+ printf '%s' "$1" | grep -qiE -- "${RULE_RE[$i]}" && return 0
138
+ done
139
+ return 1
140
+ }
141
+
142
+ gate_line_allowed() {
143
+ # $1 nội dung dòng, $2 id rule đang xét. Đúng khi dòng là giá trị mẫu hoặc có gate-allow:<id>.
144
+ local i
145
+ # Ngoại lệ tại dòng chỉ dành cho mã nguồn (có review qua MR); prompt-check.sh tắt bằng GATE_NO_INLINE_ALLOW=1
146
+ if [ "${GATE_NO_INLINE_ALLOW:-0}" != 1 ]; then
147
+ printf '%s' "$1" | grep -qiE -- "gate-allow:$2([^a-z0-9-]|$)" && return 0
148
+ fi
149
+ for i in "${!RULE_ID[@]}"; do
150
+ [ "${RULE_KIND[$i]}" = allow ] || continue
151
+ printf '%s' "$1" | grep -qiE -- "${RULE_RE[$i]}" && return 0
152
+ done
153
+ return 1
154
+ }
155
+
156
+ # ---------- Che giá trị nhạy cảm khi in và ghi log ----------
157
+ gate_mask() {
158
+ # In tối đa 72 ký tự của dòng, phần khớp regex được thay bằng 6 ký tự đầu + ***.
159
+ local text=$1 re=$2 m masked
160
+ m=$(printf '%s' "$text" | grep -oiE -- "$re" | head -n1)
161
+ if [ -n "$m" ] && [ ${#m} -gt 8 ]; then
162
+ masked=${text//"$m"/${m:0:6}***}
163
+ else
164
+ masked=$text
165
+ fi
166
+ masked=$(printf '%s' "$masked" | sed 's/^[[:space:]]*//')
167
+ [ ${#masked} -gt 72 ] && masked="${masked:0:72}..."
168
+ printf '%s' "$masked"
169
+ }
170
+
171
+ # ---------- Ghi log ----------
172
+ gate_log() {
173
+ # stage level id decision path lineno masked
174
+ local dir
175
+ dir=$(dirname "$GATE_LOG")
176
+ mkdir -p "$dir" 2>/dev/null || return 0
177
+ printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
178
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$GATE_VERSION" "$GATE_MODE" "$1" "$2" "$3" "$4" "${GATE_REPO_NAME:-?}" "$5" "$6" "$7" >> "$GATE_LOG" 2>/dev/null || true
179
+ }
180
+
181
+ # ---------- Kết quả ----------
182
+ GATE_N_BLOCK=0; GATE_N_WARN=0; GATE_N_ALLOWED=0
183
+ GATE_STAGE=${GATE_STAGE:-scan}
184
+ GATE_REPO_NAME=${GATE_REPO_NAME:-}
185
+
186
+ gate_report_finding() {
187
+ # $1 level, $2 id, $3 path, $4 lineno, $5 masked text, $6 message
188
+ local level=$1 id=$2 path=$3 ln=$4 masked=$5 msg=$6 decision tag
189
+ if gate_is_allowed "$id" "$path"; then
190
+ GATE_N_ALLOWED=$((GATE_N_ALLOWED + 1))
191
+ gate_log "$GATE_STAGE" "$level" "$id" allowed "${GATE_PATH_PREFIX:-}$path" "$ln" "$masked"
192
+ return 0
193
+ fi
194
+ if [ "$level" = block ] && [ "$GATE_MODE" != audit ]; then
195
+ GATE_N_BLOCK=$((GATE_N_BLOCK + 1)); decision=block; tag=CHAN
196
+ elif [ "$level" = block ]; then
197
+ GATE_N_WARN=$((GATE_N_WARN + 1)); decision=audit; tag=AUDIT
198
+ else
199
+ GATE_N_WARN=$((GATE_N_WARN + 1)); decision=warn; tag=CANH-BAO
200
+ fi
201
+ gate_log "$GATE_STAGE" "$level" "$id" "$decision" "${GATE_PATH_PREFIX:-}$path" "$ln" "$masked"
202
+ if [ -n "$ln" ]; then gate_say "$tag $id ${GATE_PATH_PREFIX:-}$path:$ln $msg"
203
+ else gate_say "$tag $id ${GATE_PATH_PREFIX:-}$path $msg"; fi
204
+ [ -n "$masked" ] && [ "$GATE_QUIET" != 1 ] && gate_say " > $masked"
205
+ return 0
206
+ }
207
+
208
+ # ---------- Quét một file ----------
209
+ # gate_scan_file <path> <file-chứa-các-dòng-cần-quét> [file-đầy-đủ] [kích-thước-bytes]
210
+ # file-chứa-các-dòng: các dòng được thêm (diff) hoặc toàn bộ nội dung (quét cả cây); đã bỏ dấu + ở đầu.
211
+ # file-đầy-đủ: nội dung đầy đủ để kiểm rule count/size; bỏ trống thì dùng file-chứa-các-dòng.
212
+ gate_scan_file() {
213
+ local path=$1 lines=$2 full=${3:-$2} size=${4:-} i id level kind re msg hit ln text masked n thr
214
+ gate_path_skipped "$path" && return 0
215
+
216
+ # Rule đường dẫn
217
+ if ! gate_path_exempt "$path"; then
218
+ for i in "${!RULE_ID[@]}"; do
219
+ [ "${RULE_KIND[$i]}" = path ] || continue
220
+ if printf '%s' "$path" | grep -qiE -- "${RULE_RE[$i]}"; then
221
+ gate_report_finding "${RULE_LEVEL[$i]}" "${RULE_ID[$i]}" "$path" "" "" "${RULE_MSG[$i]}"
222
+ fi
223
+ done
224
+ fi
225
+
226
+ [ -s "$lines" ] || return 0
227
+ # File nhị phân: không quét theo dòng.
228
+ if grep -qI . "$lines" 2>/dev/null; then :; else return 0; fi
229
+ if [ -z "$size" ]; then size=$(wc -c < "$full" | tr -d ' '); fi
230
+
231
+ # Rule kích thước
232
+ for i in "${!RULE_ID[@]}"; do
233
+ [ "${RULE_KIND[$i]}" = size ] || continue
234
+ thr=${RULE_RE[$i]%%|*}; re=${RULE_RE[$i]#*|}
235
+ if [ "$size" -gt "$thr" ] && printf '%s' "$path" | grep -qiE -- "$re"; then
236
+ gate_report_finding "${RULE_LEVEL[$i]}" "${RULE_ID[$i]}" "$path" "" "$size bytes" "${RULE_MSG[$i]}"
237
+ fi
238
+ done
239
+
240
+ [ "$size" -gt "$GATE_MAX_FILE" ] && return 0
241
+
242
+ # Rule đếm (trên nội dung đầy đủ)
243
+ for i in "${!RULE_ID[@]}"; do
244
+ [ "${RULE_KIND[$i]}" = count ] || continue
245
+ thr=${RULE_RE[$i]%%|*}; re=${RULE_RE[$i]#*|}
246
+ n=$(grep -ciE -- "$re" "$full" 2>/dev/null || true); n=${n:-0}
247
+ if [ "$n" -ge "$thr" ]; then
248
+ gate_report_finding "${RULE_LEVEL[$i]}" "${RULE_ID[$i]}" "$path" "" "$n dòng khớp (ngưỡng $thr)" "${RULE_MSG[$i]}"
249
+ fi
250
+ done
251
+
252
+ # Rule theo dòng (trên các dòng được thêm)
253
+ for i in "${!RULE_ID[@]}"; do
254
+ [ "${RULE_KIND[$i]}" = line ] || continue
255
+ id=${RULE_ID[$i]}; level=${RULE_LEVEL[$i]}; re=${RULE_RE[$i]}; msg=${RULE_MSG[$i]}
256
+ while IFS= read -r hit; do
257
+ [ -n "$hit" ] || continue
258
+ ln=${hit%%:*}; text=${hit#*:}
259
+ if gate_line_allowed "$text" "$id"; then
260
+ GATE_N_ALLOWED=$((GATE_N_ALLOWED + 1))
261
+ gate_log "$GATE_STAGE" "$level" "$id" allowed-inline "$path" "$ln" "$(gate_mask "$text" "$re")"
262
+ continue
263
+ fi
264
+ masked=$(gate_mask "$text" "$re")
265
+ gate_report_finding "$level" "$id" "$path" "$ln" "$masked" "$msg"
266
+ done < <(grep -niE -- "$re" "$lines" 2>/dev/null | head -n 20)
267
+ done
268
+ }
269
+
270
+ gate_summary() {
271
+ # Trả về mã thoát: 1 nếu phải chặn, 0 nếu không.
272
+ local what=${1:-thay đổi}
273
+ if [ "$GATE_N_BLOCK" -gt 0 ]; then
274
+ gate_say "Từ chối $what: $GATE_N_BLOCK vi phạm mức CHẶN, $GATE_N_WARN cảnh báo, $GATE_N_ALLOWED ngoại lệ đã khai."
275
+ gate_say "$TEXT_BLOCK"
276
+ return 1
277
+ fi
278
+ if [ "$GATE_N_WARN" -gt 0 ]; then
279
+ if [ "$GATE_MODE" = audit ]; then gate_say "Chế độ audit: $GATE_N_WARN phát hiện, đã ghi log, cho qua. $TEXT_AUDIT"
280
+ else gate_say "Cho qua với $GATE_N_WARN cảnh báo, $GATE_N_ALLOWED ngoại lệ đã khai. $TEXT_WARN"; fi
281
+ return 0
282
+ fi
283
+ [ "$GATE_QUIET" = 1 ] || gate_say "Đạt: không phát hiện vi phạm ($GATE_N_ALLOWED ngoại lệ đã khai)."
284
+ return 0
285
+ }
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env bash
2
+ # Điểm vào của Cổng kiểm soát ATBMTT (VNPTNAN_GATE).
3
+ #
4
+ # gate-scan.sh staged # quét thay đổi đã stage (pre-commit)
5
+ # gate-scan.sh range <old> <new> # quét các commit trong khoảng (pre-push); <old> có thể là 0000... (nhánh mới)
6
+ # gate-scan.sh receive # đọc "old new ref" từ stdin (pre-push và pre-receive của GitLab)
7
+ # gate-scan.sh commit <sha> # quét một commit
8
+ # gate-scan.sh tree [thư-mục] # quét toàn bộ cây làm việc (đánh giá repo cũ, CI)
9
+ # gate-scan.sh deps [thư-mục] # kiểm dependency và giấy phép
10
+ #
11
+ # Mã thoát: 0 cho qua, 1 từ chối, 2 lỗi cấu hình.
12
+ set -u
13
+ ENGINE_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
14
+ # shellcheck source=gate-lib.sh
15
+ . "$ENGINE_DIR/gate-lib.sh"
16
+
17
+ mode=${1:-}
18
+ shift || true
19
+
20
+ ZERO='^0+$'
21
+ TMP=$(mktemp -d 2>/dev/null || mktemp -d -t gate)
22
+ trap 'rm -rf "$TMP"' EXIT
23
+
24
+ repo_root() { git rev-parse --show-toplevel 2>/dev/null || pwd; }
25
+
26
+ init() {
27
+ local root
28
+ root=$(repo_root)
29
+ GATE_REPO_NAME=${GL_PROJECT_PATH:-$(basename "$root")}
30
+ gate_load_conf "$root"
31
+ gate_load_rules || exit 2
32
+ gate_load_allow "$root/.vnptnan-gate/allow.rules"
33
+ # Trên server, file ngoại lệ nằm trong commit mới, đọc từ nhánh được đẩy nếu có.
34
+ if [ "$GATE_SERVER" = 1 ] && [ -n "${GATE_ALLOW_REV:-}" ]; then
35
+ if git cat-file -e "$GATE_ALLOW_REV:.vnptnan-gate/allow.rules" 2>/dev/null; then
36
+ git show "$GATE_ALLOW_REV:.vnptnan-gate/allow.rules" > "$TMP/allow.rules" && gate_load_allow "$TMP/allow.rules"
37
+ fi
38
+ fi
39
+ }
40
+
41
+ # Các dòng được thêm của một diff, bỏ dấu +, bỏ \r. $1 ... là đối số cho git diff.
42
+ added_lines() {
43
+ git diff --no-color --no-ext-diff -U0 --diff-filter=ACMR "$@" 2>/dev/null \
44
+ | grep -E '^\+' | grep -vE '^\+\+\+ ' | sed 's/^+//; s/\r$//'
45
+ }
46
+
47
+ scan_staged() {
48
+ GATE_STAGE=pre-commit
49
+ local path blob
50
+ while IFS= read -r -d '' path; do
51
+ [ -n "$path" ] || continue
52
+ added_lines --cached -- "$path" > "$TMP/lines"
53
+ git show ":$path" > "$TMP/full" 2>/dev/null || : > "$TMP/full"
54
+ gate_scan_file "$path" "$TMP/lines" "$TMP/full"
55
+ done < <(git diff --cached --name-only --diff-filter=ACMR -z)
56
+ }
57
+
58
+ scan_commit() {
59
+ # $1 sha. Commit merge chỉ quét những dòng merge tự thêm (so với cha đầu tiên).
60
+ local sha=$1 path short
61
+ short=$(git rev-parse --short "$sha" 2>/dev/null || echo "$sha")
62
+ while IFS= read -r -d '' path; do
63
+ [ -n "$path" ] || continue
64
+ printf '%s' "$path" | grep -qiE '(^|/)(package\.json|[^/]+\.csproj|Directory\.Packages\.props|packages\.config)$' && DEPS_TOUCHED=1
65
+ git diff --no-color --no-ext-diff -U0 --diff-filter=ACMR "$sha^" "$sha" -- "$path" 2>/dev/null \
66
+ | grep -E '^\+' | grep -vE '^\+\+\+ ' | sed 's/^+//; s/\r$//' > "$TMP/lines"
67
+ if ! git rev-parse -q --verify "$sha^" >/dev/null 2>&1; then
68
+ # Commit gốc: mọi dòng đều là dòng thêm.
69
+ git show "$sha:$path" 2>/dev/null | sed 's/\r$//' > "$TMP/lines"
70
+ fi
71
+ git show "$sha:$path" > "$TMP/full" 2>/dev/null || : > "$TMP/full"
72
+ GATE_PATH_PREFIX="$short:" gate_scan_file "$path" "$TMP/lines" "$TMP/full"
73
+ done < <(git diff-tree -r -z --no-commit-id --name-only --diff-filter=ACMR --root "$sha" 2>/dev/null)
74
+ }
75
+
76
+ scan_range() {
77
+ # $1 old, $2 new, $3 ref (tuỳ chọn)
78
+ local old=$1 new=$2 ref=${3:-} list n=0
79
+ if printf '%s' "$new" | grep -qE "$ZERO"; then return 0; fi # xoá nhánh: không có gì để quét
80
+ if printf '%s' "$old" | grep -qE "$ZERO"; then
81
+ # Nhánh mới: chỉ các commit chưa có ở bất kỳ ref nào khác.
82
+ if [ "$GATE_SERVER" = 1 ]; then list=$(git rev-list "$new" --not --all 2>/dev/null)
83
+ else list=$(git rev-list "$new" --not --remotes 2>/dev/null); fi
84
+ else
85
+ list=$(git rev-list "$old..$new" 2>/dev/null)
86
+ fi
87
+ [ -n "$list" ] || return 0
88
+ n=$(printf '%s\n' "$list" | wc -l | tr -d ' ')
89
+ if [ "$n" -gt "$GATE_MAX_COMMITS" ]; then
90
+ gate_say "Khoảng đẩy có $n commit, chỉ quét $GATE_MAX_COMMITS commit mới nhất (MAX_COMMITS). Phần còn lại cần quét bằng: vnptnan-gate scan tree"
91
+ list=$(printf '%s\n' "$list" | head -n "$GATE_MAX_COMMITS")
92
+ fi
93
+ [ -n "$ref" ] && [ "$GATE_QUIET" != 1 ] && gate_say "Quét $n commit lên $ref"
94
+ GATE_ALLOW_REV=$new
95
+ while IFS= read -r sha; do [ -n "$sha" ] && scan_commit "$sha"; done <<< "$list"
96
+ # Dependency: chỉ trên máy dev (server không có node_modules/NuGet cache) và chỉ khi khoảng đẩy có đổi file khai báo package.
97
+ if [ "$GATE_DEPS" != off ] && [ "$GATE_SERVER" != 1 ] && [ "${DEPS_TOUCHED:-0}" = 1 ]; then
98
+ deps_check "$(repo_root)"
99
+ fi
100
+ }
101
+
102
+ scan_receive() {
103
+ local old new ref
104
+ GATE_STAGE=${GATE_STAGE_NAME:-pre-push}
105
+ [ "$GATE_SERVER" = 1 ] && GATE_STAGE=pre-receive
106
+ local a b c d
107
+ while read -r a b c d; do
108
+ [ -n "$c" ] || continue
109
+ if [ -n "$d" ]; then
110
+ # pre-push của git: local_ref local_sha remote_ref remote_sha
111
+ scan_range "$d" "$b" "$c"
112
+ else
113
+ # pre-receive của GitLab: old new ref
114
+ scan_range "$a" "$b" "$c"
115
+ fi
116
+ done
117
+ }
118
+
119
+ scan_tree() {
120
+ local dir=${1:-.} f rel size
121
+ GATE_STAGE=tree
122
+ dir=$(cd "$dir" && pwd)
123
+ if git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
124
+ # File git theo dõi + file chưa theo dõi nhưng không bị ignore.
125
+ git -C "$dir" ls-files -z --cached --others --exclude-standard
126
+ else
127
+ find "$dir" -type f -print0
128
+ fi > "$TMP/files"
129
+ while IFS= read -r -d '' f; do
130
+ [ -n "$f" ] || continue
131
+ rel=${f#"$dir"/}
132
+ [ -f "$dir/$rel" ] || continue
133
+ size=$(wc -c < "$dir/$rel" | tr -d ' ')
134
+ sed 's/\r$//' "$dir/$rel" > "$TMP/lines" 2>/dev/null || continue
135
+ gate_scan_file "$rel" "$TMP/lines" "$TMP/lines" "$size"
136
+ done < "$TMP/files"
137
+ [ "$GATE_DEPS" != off ] && deps_check "$dir"
138
+ }
139
+
140
+ deps_check() {
141
+ # shellcheck source=gate-deps.sh
142
+ . "$ENGINE_DIR/gate-deps.sh"
143
+ gate_deps_scan "$1"
144
+ }
145
+
146
+ case "$mode" in
147
+ staged) init; scan_staged; gate_summary "commit" ;;
148
+ range) init; GATE_STAGE=pre-push; scan_range "${1:?old}" "${2:?new}" "${3:-}"; gate_summary "push" ;;
149
+ receive) init; scan_receive; gate_summary "push" ;;
150
+ commit) init; GATE_STAGE=commit; scan_commit "${1:?sha}"; gate_summary "commit" ;;
151
+ tree) init; scan_tree "${1:-.}"; gate_summary "cây làm việc" ;;
152
+ deps) init; GATE_STAGE=deps; deps_check "${1:-$(repo_root)}"; gate_summary "dependency" ;;
153
+ version) echo "VNPTNAN_GATE $GATE_VERSION" ;;
154
+ *) echo "Cách dùng: gate-scan.sh staged | range <old> <new> | receive | commit <sha> | tree [dir] | deps [dir]" >&2; exit 2 ;;
155
+ esac
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bash
2
+ # Kiểm tra prompt TRƯỚC KHI gửi tới mô hình — hook UserPromptSubmit của Claude Code và Codex.
3
+ # Dùng chung rule với cổng commit (rules/gate.rules): rule kind=line và kind=count áp lên nội dung prompt.
4
+ #
5
+ # stdin : JSON của sự kiện UserPromptSubmit (có trường "prompt")
6
+ # stdout: {"decision":"block","reason":"..."} khi phải chặn (Claude Code và Codex cùng hiểu); rỗng khi cho qua
7
+ # Mã thoát luôn 0: hook lỗi thì cho qua (cùng cách Codex xử lý), lần lỗi được ghi log.
8
+ #
9
+ # bash prompt-check.sh --agent claude|codex
10
+ #
11
+ # Khác với cổng commit, ở đây KHÔNG áp dụng ngoại lệ: không đọc .vnptnan-gate/allow.rules và không nhận
12
+ # "gate-allow:" trong prompt, vì prompt không qua merge request nên không có ai duyệt ngoại lệ.
13
+ # Antigravity không có sự kiện trước khi gửi prompt có quyền chặn, nên không dùng được script này.
14
+
15
+ # Không tin biến môi trường: chỉ đọc gate.conf ở thư mục cài (chỉ admin ghi được)
16
+ unset VNPTNAN_GATE_HOME GATE_ROOT GATE_MODE GATE_DEPS GATE_DEPS_AUDIT GATE_SKIP_PATHS GATE_SKIP_EXTRA \
17
+ GATE_MAX_COMMITS GATE_MAX_FILE GATE_QUIET GATE_SERVER
18
+ set -u
19
+ ENGINE_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
20
+ AGENT=claude
21
+ [ "${1:-}" = "--agent" ] && AGENT=${2:-claude}
22
+
23
+ TMP=$(mktemp -d 2>/dev/null || mktemp -d -t gateprompt)
24
+ trap 'rm -rf "$TMP"' EXIT
25
+ cat > "$TMP/in.json"
26
+
27
+ # Lấy trường "prompt" và giải mã chuỗi JSON (\n, \", \\, \uXXXX kể cả cặp surrogate) bằng perl có sẵn trong Git for Windows,
28
+ # không cần module ngoài. Không có trường prompt thì cho qua.
29
+ perl -e '
30
+ local $/; my $j = <STDIN>;
31
+ exit 3 unless $j =~ /"prompt"\s*:\s*"((?:[^"\\]|\\.)*)"/s;
32
+ my $s = $1; my $out = "";
33
+ while ($s =~ /\G(?:([^\\]+)|\\u([0-9a-fA-F]{4})(?:\\u([0-9a-fA-F]{4}))?|\\(.))/gcs) {
34
+ if (defined $1) { $out .= $1; next }
35
+ if (defined $2) {
36
+ my $c = hex $2;
37
+ if ($c >= 0xD800 && $c <= 0xDBFF && defined $3) { $c = 0x10000 + (($c - 0xD800) << 10) + (hex($3) - 0xDC00) }
38
+ elsif (defined $3) { $out .= chr_utf8($c); $c = hex $3 }
39
+ $out .= chr_utf8($c); next
40
+ }
41
+ my %m = (n => "\n", t => "\t", r => "", b => "", f => "", "/" => "/", "\\" => "\\", "\"" => "\"");
42
+ $out .= exists $m{$4} ? $m{$4} : $4;
43
+ }
44
+ sub chr_utf8 { my $c = shift; my $x = chr($c); utf8::encode($x); return $x }
45
+ print $out;
46
+ ' < "$TMP/in.json" > "$TMP/prompt.txt" 2>/dev/null
47
+ rc=$?
48
+ [ "$rc" -eq 3 ] && exit 0
49
+ if [ "$rc" -ne 0 ]; then
50
+ echo "[VNPTNAN_GATE] prompt-check: không đọc được JSON đầu vào, cho qua" >&2
51
+ exit 0
52
+ fi
53
+ [ -s "$TMP/prompt.txt" ] || exit 0
54
+
55
+ # shellcheck source=gate-lib.sh
56
+ . "$ENGINE_DIR/gate-lib.sh"
57
+ gate_load_conf ""
58
+ gate_load_rules >/dev/null 2>&1 || { echo "[VNPTNAN_GATE] prompt-check: không nạp được rule, cho qua" >&2; exit 0; }
59
+ GATE_STAGE="prompt-$AGENT"
60
+ GATE_REPO_NAME=$(basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
61
+ GATE_QUIET=1
62
+ GATE_NO_INLINE_ALLOW=1 # gate-lib: bỏ qua "gate-allow:" trên dòng
63
+ GATE_SKIP_PATHS='^$' # không bỏ qua gì: đường dẫn giả "<prompt>" phải được quét
64
+
65
+ # Quét; thu các dòng thông báo để làm lý do chặn
66
+ gate_scan_file "<prompt>" "$TMP/prompt.txt" "$TMP/prompt.txt" "$(wc -c < "$TMP/prompt.txt" | tr -d ' ')" 2> "$TMP/findings.txt"
67
+
68
+ if [ "$GATE_N_BLOCK" -gt 0 ]; then
69
+ {
70
+ echo "Cổng kiểm soát ATBMTT đã chặn prompt này trước khi gửi tới AI (Bộ quy tắc sử dụng AI, Điều 5: dữ liệu mức C3 không được đưa vào AI)."
71
+ sed -n 's/^\[VNPTNAN_GATE\] CHAN \([a-z0-9-]*\) <prompt>\(:[0-9]*\)\{0,1\} /- \1: /p' "$TMP/findings.txt" | head -n 8
72
+ echo "Gỡ mật khẩu, khoá, dữ liệu khách hàng khỏi prompt (thay bằng biến môi trường, dữ liệu giả hoặc mô tả bằng lời) rồi gửi lại."
73
+ } > "$TMP/reason.txt"
74
+ perl -e 'local $/; my $r = <STDIN>; $r =~ s/\\/\\\\/g; $r =~ s/"/\\"/g; $r =~ s/\r//g; $r =~ s/\n/\\n/g; $r =~ s/\t/\\t/g;
75
+ print "{\"decision\":\"block\",\"reason\":\"$r\"}\n"' < "$TMP/reason.txt"
76
+ fi
77
+ exit 0
package/gate.conf ADDED
@@ -0,0 +1,13 @@
1
+ # Cấu hình cấp bộ công cụ. Dự án ghi đè bằng .vnptnan-gate/gate.conf; biến môi trường GATE_* ghi đè tất cả.
2
+ # MODE: enforce (từ chối commit/push vi phạm mức block) | audit (chỉ ghi log; dùng trong 2 tuần thí điểm)
3
+ MODE=enforce
4
+ # DEPS: block | warn | off — mức tối đa cho phát hiện về dependency/giấy phép
5
+ DEPS=warn
6
+ # DEPS_AUDIT: 1 = chạy npm audit / dotnet list package --vulnerable (cần mạng, chậm); mặc định tắt trong hook, bật trong CI
7
+ DEPS_AUDIT=0
8
+ # Số commit tối đa quét trong một lần push
9
+ MAX_COMMITS=500
10
+ # File lớn hơn (bytes) không quét theo dòng
11
+ MAX_FILE=5242880
12
+ # SKIP_PATHS_EXTRA: regex đường dẫn thêm vào danh sách bỏ qua mặc định (thường đặt trong .vnptnan-gate/gate.conf của dự án)
13
+ #SKIP_PATHS_EXTRA=(^|/)legacy-dump/
@@ -0,0 +1,10 @@
1
+ #!/bin/sh
2
+ # Hook commit-msg của VNPTNAN_GATE: ghi vết commit có AI tham gia để thống kê và kiểm toán.
3
+ # Nếu thông điệp có trailer Co-Authored-By của một coding agent mà chưa có trailer AI-Assisted, tự thêm "AI-Assisted: <tool>".
4
+ # Không bao giờ từ chối commit.
5
+ msg=$1
6
+ [ -f "$msg" ] || exit 0
7
+ if grep -qiE '^AI-Assisted:' "$msg"; then exit 0; fi
8
+ tool=$(grep -iE '^Co-Authored-By:.*(claude|codex|openai|antigravity|gemini|copilot)' "$msg" | head -n1 | sed -E 's/^Co-Authored-By:[[:space:]]*//I; s/[[:space:]]*<.*//')
9
+ if [ -n "$tool" ]; then printf '\nAI-Assisted: %s\n' "$tool" >> "$msg"; fi
10
+ exit 0
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+ # Hook pre-commit của VNPTNAN_GATE: quét thay đổi đã stage trước khi commit.
3
+ # Cài qua git config hook.vnptnan-gate-commit.* (cấp máy) hoặc copy vào .git/hooks/pre-commit của repo.
4
+ # Chống hạ cổng bằng biến môi trường (vd. GATE_MODE=audit git commit): hook chỉ tin gate.conf.
5
+ unset VNPTNAN_GATE_HOME GATE_ROOT GATE_MODE GATE_DEPS GATE_DEPS_AUDIT GATE_SKIP_PATHS GATE_SKIP_EXTRA GATE_MAX_COMMITS GATE_MAX_FILE GATE_QUIET GATE_SERVER
6
+ root=$(cd "$(dirname "$0")/.." && pwd)
7
+ GATE_STAGE_NAME=pre-commit exec bash "$root/engine/gate-scan.sh" staged
package/hooks/pre-push ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+ # Hook pre-push của VNPTNAN_GATE: quét mọi commit sắp rời máy, kể cả commit tri thức của VNPTNAN_HARNESS (dùng lệnh plumbing, không qua pre-commit).
3
+ # stdin: <local ref> <local sha> <remote ref> <remote sha>, một dòng mỗi ref.
4
+ # Chống hạ cổng bằng biến môi trường (vd. GATE_MODE=audit git commit): hook chỉ tin gate.conf.
5
+ unset VNPTNAN_GATE_HOME GATE_ROOT GATE_MODE GATE_DEPS GATE_DEPS_AUDIT GATE_SKIP_PATHS GATE_SKIP_EXTRA GATE_MAX_COMMITS GATE_MAX_FILE GATE_QUIET GATE_SERVER
6
+ root=$(cd "$(dirname "$0")/.." && pwd)
7
+ GATE_STAGE_NAME=pre-push exec bash "$root/engine/gate-scan.sh" receive
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env bash
2
+ # Cài VNPTNAN_GATE làm server hook toàn cục trên máy chủ GitLab tự host (Linux package / Omnibus, mọi bậc kể cả Free).
3
+ # Chạy bằng root trên máy chủ GitLab:
4
+ # sudo bash installer/install-server.sh [-mode enforce|audit]
5
+ # Việc làm: copy bộ công cụ vào /opt/vnptnan-gate (root sở hữu, git chỉ đọc), tạo thư mục custom_hooks/pre-receive.d,
6
+ # đặt symlink 10-vnptnan-gate, tạo /var/log/vnptnan-gate cho tài khoản git ghi. Không cần khởi động lại GitLab.
7
+ # Tài liệu GitLab: Administration > Git server hooks (thư mục custom_hooks toàn cục: gitaly['configuration'][:hooks][:custom_hooks_dir],
8
+ # mặc định /var/opt/gitlab/gitaly/custom_hooks).
9
+ set -eu
10
+ MODE=enforce
11
+ [ "${1:-}" = "-mode" ] && MODE=${2:-enforce}
12
+ SRC=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
13
+ DEST=/opt/vnptnan-gate
14
+ HOOKS_DIR=${GITLAB_CUSTOM_HOOKS_DIR:-/var/opt/gitlab/gitaly/custom_hooks}
15
+ GIT_USER=${GITLAB_GIT_USER:-git}
16
+
17
+ [ "$(id -u)" = 0 ] || { echo "Chạy bằng root."; exit 2; }
18
+ id "$GIT_USER" >/dev/null 2>&1 || { echo "Không có tài khoản $GIT_USER. Đặt GITLAB_GIT_USER."; exit 2; }
19
+
20
+ rm -rf "$DEST"; mkdir -p "$DEST"
21
+ cp -r "$SRC"/. "$DEST"/; rm -rf "$DEST/.git"
22
+ sed -i "s/^MODE=.*/MODE=$MODE/" "$DEST/gate.conf"
23
+ chown -R root:root "$DEST"; chmod -R a+rX "$DEST"; chmod +x "$DEST"/server/pre-receive "$DEST"/engine/*.sh
24
+
25
+ mkdir -p "$HOOKS_DIR/pre-receive.d"
26
+ ln -sfn "$DEST/server/pre-receive" "$HOOKS_DIR/pre-receive.d/10-vnptnan-gate"
27
+ chown -R "$GIT_USER":"$GIT_USER" "$HOOKS_DIR"
28
+
29
+ mkdir -p /var/log/vnptnan-gate; chown "$GIT_USER":"$GIT_USER" /var/log/vnptnan-gate; chmod 750 /var/log/vnptnan-gate
30
+
31
+ echo "Đã cài VNPTNAN_GATE $(cat "$DEST/VERSION") làm server hook toàn cục (MODE=$MODE)."
32
+ echo "Kiểm tra: đẩy một commit chứa 'AKIAIOSFODNN7EXAMPLE' lên nhánh thử; GitLab phải từ chối với dòng GL-HOOK-ERR."
33
+ echo "Log: /var/log/vnptnan-gate/gate.log. Để quét định kỳ: xem ci/.gitlab-ci.yml khi bật CI."