synapse-hub 2.2.2 → 2.2.4
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/.agents/rules/author-integrity-guard.md +9 -10
- package/.agents/rules/git-push-protocol.md +1 -1
- package/.agents/rules/lessons-learned.md +3 -3
- package/.agents/rules/skill-precedence.md +1 -1
- package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +3 -0
- package/.agents/skills/code-refactoring-refactor-clean/resources/implementation-playbook.md +9 -7
- package/.agents/skills/security-dast-sentinel/SKILL.md +3 -0
- package/.cursorrules +7 -8
- package/.windsurfrules +7 -8
- package/AGENTS.md +3 -3
- package/CLAUDE.md +4 -5
- package/README.md +32 -26
- package/package.json +1 -1
- package/skills_manifest.json +2 -2
- package/tools/author_integrity.py +10 -4
- package/tools/browser_helpers.js +2 -2
- package/tools/cache_resolver.py +16 -5
- package/tools/cli.py +1 -1
- package/tools/distill_log.py +19 -10
- package/tools/export_postman_collection.py +29 -7
- package/tools/generate_jmx_plan.py +42 -10
- package/tools/infer_linter.py +45 -15
- package/tools/jtl_analyzer.py +1 -1
- package/tools/mcp_excel_server.ps1 +1 -1
- package/tools/mcp_server.py +1 -1
- package/tools/micro_mutator.py +78 -7
- package/tools/repo_map.py +90 -56
- package/tools/safety_firewall.py +15 -2
- package/tools/setup_project.py +43 -13
- package/tools/skillspector.py +8 -8
- package/tools/verify_all.py +55 -47
package/tools/repo_map.py
CHANGED
|
@@ -21,10 +21,11 @@ HUB_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
|
|
21
21
|
OUTPUT_TXT = os.path.join(SCRIPT_DIR, "repo_map.txt")
|
|
22
22
|
OUTPUT_JSON = os.path.join(SCRIPT_DIR, "repo_map.json")
|
|
23
23
|
|
|
24
|
-
# Regex patterns for Groovy classes and methods
|
|
25
|
-
CLASS_PATTERN = re.compile(r'^\s*(?:public\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?', re.MULTILINE)
|
|
24
|
+
# Regex patterns for Groovy/Java classes and methods
|
|
25
|
+
CLASS_PATTERN = re.compile(r'^\s*(?:public\s+|export\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?', re.MULTILINE)
|
|
26
|
+
PY_CLASS_PATTERN = re.compile(r'^\s*class\s+(\w+)(?:\(([^)]*)\))?\s*:', re.MULTILINE)
|
|
26
27
|
METHOD_PATTERN = re.compile(
|
|
27
|
-
r'^\s*(?:@\w+\s+)*(?:public\s+|protected\s+|private\s+)?(?:static\s+)?(?:def\s+|void\s+|[\w<>\[\],]+\s+)?(\w+)\s*\(([^)]*)\)\s*(?:throws\s+[\w,\s]+)?\s*\{',
|
|
28
|
+
r'^\s*(?:@\w+\s+)*(?:public\s+|protected\s+|private\s+)?(?:static\s+)?(?:async\s+)?(?:def\s+|void\s+|[\w<>\[\],]+\s+)?(\w+)\s*\(([^)]*)\)\s*(?:throws\s+[\w,\s]+)?\s*\{',
|
|
28
29
|
re.MULTILINE
|
|
29
30
|
)
|
|
30
31
|
DOC_PATTERN = re.compile(r'/\*\*(.*?)\*/', re.DOTALL)
|
|
@@ -35,7 +36,7 @@ def resolve_target_repo(cli_target=None, cli_repo=None):
|
|
|
35
36
|
Resolves the target repository path for repo mapping:
|
|
36
37
|
1. Explicit CLI flag: --repo / --path or positional target.
|
|
37
38
|
2. Environment variable: TARGET_PROJECT_DIR.
|
|
38
|
-
3. Current Working Directory (if different from Hub and has Keywords
|
|
39
|
+
3. Current Working Directory (if different from Hub and has Keywords/, pages/, or tests/).
|
|
39
40
|
4. Default fallback: QA Hub root (parent of tools/).
|
|
40
41
|
"""
|
|
41
42
|
chosen = cli_repo or cli_target
|
|
@@ -51,14 +52,15 @@ def resolve_target_repo(cli_target=None, cli_repo=None):
|
|
|
51
52
|
|
|
52
53
|
cwd = os.path.abspath(os.getcwd())
|
|
53
54
|
if cwd.lower() != HUB_ROOT.lower():
|
|
54
|
-
|
|
55
|
+
markers = ("Keywords", "Scripts", "pages", "tests", "src/pages", "e2e")
|
|
56
|
+
if any(os.path.exists(os.path.join(cwd, m)) for m in markers):
|
|
55
57
|
return cwd
|
|
56
58
|
|
|
57
59
|
return HUB_ROOT
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
def extract_class_info(file_path, repo_root=None):
|
|
61
|
-
"""Parses a Groovy file and extracts class name
|
|
63
|
+
"""Parses a Groovy, TypeScript, JavaScript, or Python file and extracts class name and methods."""
|
|
62
64
|
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
63
65
|
content = f.read()
|
|
64
66
|
|
|
@@ -68,48 +70,72 @@ def extract_class_info(file_path, repo_root=None):
|
|
|
68
70
|
except ValueError:
|
|
69
71
|
rel_path = os.path.abspath(file_path)
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
if not class_match:
|
|
73
|
-
return None
|
|
74
|
-
|
|
75
|
-
class_name = class_match.group(1)
|
|
76
|
-
extends_name = class_match.group(2) or ""
|
|
77
|
-
|
|
73
|
+
ext = os.path.splitext(file_path)[1].lower()
|
|
78
74
|
methods = []
|
|
79
|
-
control_keywords = {"if", "for", "while", "switch", "catch", "synchronized"}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if
|
|
75
|
+
control_keywords = {"if", "for", "while", "switch", "catch", "synchronized", "constructor"}
|
|
76
|
+
|
|
77
|
+
if ext in (".groovy", ".java", ".ts", ".js"):
|
|
78
|
+
class_match = CLASS_PATTERN.search(content)
|
|
79
|
+
if not class_match:
|
|
80
|
+
return None
|
|
81
|
+
class_name = class_match.group(1)
|
|
82
|
+
extends_name = class_match.group(2) or ""
|
|
83
|
+
|
|
84
|
+
lines = content.splitlines()
|
|
85
|
+
for idx, line in enumerate(lines):
|
|
86
|
+
line_clean = line.strip()
|
|
87
|
+
if not line_clean or line_clean.startswith(("//", "/*", "*")):
|
|
92
88
|
continue
|
|
93
89
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
90
|
+
if "=" in line_clean:
|
|
91
|
+
eq_pos = line_clean.find("=")
|
|
92
|
+
paren_pos = line_clean.find("(")
|
|
93
|
+
if paren_pos != -1 and eq_pos < paren_pos:
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
if "(" in line_clean and ")" in line_clean:
|
|
97
|
+
has_brace = "{" in line_clean or (idx + 1 < len(lines) and lines[idx + 1].strip().startswith("{"))
|
|
98
|
+
if has_brace:
|
|
99
|
+
m = re.search(r'(?:public\s+|protected\s+|private\s+|static\s+|async\s+|def\s+|void\s+|[\w<>\[\],]+\s+)*([a-zA-Z_]\w*)\s*\(([^)]*)\)', line_clean)
|
|
100
|
+
if m:
|
|
101
|
+
name = m.group(1)
|
|
102
|
+
params = m.group(2).strip()
|
|
103
|
+
if name not in control_keywords and name != class_name:
|
|
104
|
+
comment = ""
|
|
105
|
+
if idx > 0 and lines[idx - 1].strip().startswith("//"):
|
|
106
|
+
comment = lines[idx - 1].strip().lstrip("/ ").strip()
|
|
107
|
+
methods.append({
|
|
108
|
+
"name": name,
|
|
109
|
+
"params": re.sub(r'\s+', ' ', params),
|
|
110
|
+
"comment": comment
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
elif ext == ".py":
|
|
114
|
+
class_match = PY_CLASS_PATTERN.search(content)
|
|
115
|
+
if not class_match:
|
|
116
|
+
return None
|
|
117
|
+
class_name = class_match.group(1)
|
|
118
|
+
extends_name = class_match.group(2) or ""
|
|
119
|
+
|
|
120
|
+
lines = content.splitlines()
|
|
121
|
+
for idx, line in enumerate(lines):
|
|
122
|
+
line_clean = line.strip()
|
|
123
|
+
if line_clean.startswith("def "):
|
|
124
|
+
m = re.search(r'def\s+([a-zA-Z_]\w*)\s*\(([^)]*)\):', line_clean)
|
|
100
125
|
if m:
|
|
101
126
|
name = m.group(1)
|
|
102
|
-
params = m.group(2).strip()
|
|
103
|
-
if
|
|
104
|
-
# Extract preceding single-line comment if any
|
|
127
|
+
params = m.group(2).replace("self,", "").replace("self", "").strip()
|
|
128
|
+
if not name.startswith("__") and name not in control_keywords:
|
|
105
129
|
comment = ""
|
|
106
|
-
if idx
|
|
107
|
-
comment = lines[idx
|
|
130
|
+
if idx + 1 < len(lines) and '"""' in lines[idx + 1]:
|
|
131
|
+
comment = lines[idx + 1].replace('"""', '').strip()
|
|
108
132
|
methods.append({
|
|
109
133
|
"name": name,
|
|
110
134
|
"params": re.sub(r'\s+', ' ', params),
|
|
111
135
|
"comment": comment
|
|
112
136
|
})
|
|
137
|
+
else:
|
|
138
|
+
return None
|
|
113
139
|
|
|
114
140
|
# Deduplicate methods preserving order
|
|
115
141
|
seen = set()
|
|
@@ -120,6 +146,7 @@ def extract_class_info(file_path, repo_root=None):
|
|
|
120
146
|
seen.add(sig)
|
|
121
147
|
unique_methods.append(meth)
|
|
122
148
|
|
|
149
|
+
lines = content.splitlines()
|
|
123
150
|
return {
|
|
124
151
|
"rel_path": rel_path,
|
|
125
152
|
"class_name": class_name,
|
|
@@ -131,13 +158,7 @@ def extract_class_info(file_path, repo_root=None):
|
|
|
131
158
|
|
|
132
159
|
|
|
133
160
|
def build_repo_map(target_repo=None, output_txt=None, output_json=None):
|
|
134
|
-
"""Scans
|
|
135
|
-
try:
|
|
136
|
-
from author_integrity import assert_integrity_or_raise
|
|
137
|
-
assert_integrity_or_raise()
|
|
138
|
-
except Exception:
|
|
139
|
-
pass
|
|
140
|
-
|
|
161
|
+
"""Scans Page Classes across polyglot targets (Groovy, TypeScript, Python) and builds map."""
|
|
141
162
|
repo = target_repo or resolve_target_repo()
|
|
142
163
|
txt_dest = output_txt or OUTPUT_TXT
|
|
143
164
|
json_dest = output_json or OUTPUT_JSON
|
|
@@ -146,18 +167,31 @@ def build_repo_map(target_repo=None, output_txt=None, output_json=None):
|
|
|
146
167
|
total_raw_bytes = 0
|
|
147
168
|
total_raw_lines = 0
|
|
148
169
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
170
|
+
candidate_dirs = [
|
|
171
|
+
os.path.join(repo, "Keywords"),
|
|
172
|
+
os.path.join(repo, "pages"),
|
|
173
|
+
os.path.join(repo, "page-objects"),
|
|
174
|
+
os.path.join(repo, "page_objects"),
|
|
175
|
+
os.path.join(repo, "src", "pages"),
|
|
176
|
+
os.path.join(repo, "tests", "pages"),
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
scanned_files = set()
|
|
180
|
+
for d in candidate_dirs:
|
|
181
|
+
if os.path.isdir(d):
|
|
182
|
+
for root, _, files in os.walk(d):
|
|
183
|
+
if any(ignored in root for ignored in (".git", "node_modules", "__pycache__", ".venv")):
|
|
184
|
+
continue
|
|
185
|
+
for fname in sorted(files):
|
|
186
|
+
if fname.endswith((".groovy", ".ts", ".js", ".py", ".java")):
|
|
187
|
+
full_path = os.path.join(root, fname)
|
|
188
|
+
if full_path not in scanned_files:
|
|
189
|
+
scanned_files.add(full_path)
|
|
190
|
+
info = extract_class_info(full_path, repo_root=repo)
|
|
191
|
+
if info and info.get("methods"):
|
|
192
|
+
classes.append(info)
|
|
193
|
+
total_raw_bytes += info["raw_size_bytes"]
|
|
194
|
+
total_raw_lines += info["line_count"]
|
|
161
195
|
|
|
162
196
|
# Sort classes: BasePage first, then alphabetical
|
|
163
197
|
classes.sort(key=lambda c: (0 if c["class_name"] == "BasePage" else 1, c["class_name"]))
|
package/tools/safety_firewall.py
CHANGED
|
@@ -41,6 +41,13 @@ DANGEROUS_ACTION_KEYWORDS = [
|
|
|
41
41
|
r"\breset\b",
|
|
42
42
|
r"\btransfer\b",
|
|
43
43
|
r"\bformat\b",
|
|
44
|
+
# Multilingual & Vietnamese destructive keywords
|
|
45
|
+
r"x[oóòỏõọ][aáàảãạ]",
|
|
46
|
+
r"x[oóòỏõọ]a\s+t[aáàảãạ]t\s+c[aảáàãạ]",
|
|
47
|
+
r"h[uủúùũụ]y",
|
|
48
|
+
r"thanh\s+to[aáàảãạ]n",
|
|
49
|
+
r"r[uúùủũụ]t\s+ti[eềéèẻẽẹ]n",
|
|
50
|
+
r"n[aạáàảã]p\s+ti[eềéèẻẽẹ]n",
|
|
44
51
|
]
|
|
45
52
|
|
|
46
53
|
DANGEROUS_SELECTOR_PATTERNS = [
|
|
@@ -50,6 +57,8 @@ DANGEROUS_SELECTOR_PATTERNS = [
|
|
|
50
57
|
r"\.p-button-danger\b",
|
|
51
58
|
r"\[aria-label\*=['\"]?delete['\"]?\]",
|
|
52
59
|
r"button[#\.\-_a-zA-Z0-9]*[_\-]*(?:del|delete|purge|remove|wipe|drop)",
|
|
60
|
+
r":(?:has-text|text)\s*\(\s*['\"][^'\"]*(?:delete|remove|wipe|purge|pay|x[oóòỏõọ][aáàảãạ]|h[uủúùũụ]y|thanh\s*to[aáàảãạ]n)[^'\"]*['\"]\s*\)",
|
|
61
|
+
r"text=['\"][^'\"]*(?:delete|remove|wipe|purge|pay|x[oóòỏõọ][aáàảãạ]|h[uủúùũụ]y|thanh\s*to[aáàảãạ]n)",
|
|
53
62
|
]
|
|
54
63
|
|
|
55
64
|
COMPILED_TEXT_REGEX = re.compile("|".join(DANGEROUS_ACTION_KEYWORDS), re.IGNORECASE)
|
|
@@ -84,8 +93,12 @@ def is_action_dangerous(action_type: str, text: str = "", selector: str = "") ->
|
|
|
84
93
|
return True, f"Element text contains dangerous keyword: '{match}'"
|
|
85
94
|
|
|
86
95
|
# Check selector
|
|
87
|
-
if sel_clean
|
|
88
|
-
|
|
96
|
+
if sel_clean:
|
|
97
|
+
if COMPILED_SEL_REGEX.search(sel_clean):
|
|
98
|
+
return True, f"Element selector matches destructive pattern: '{sel_clean}'"
|
|
99
|
+
if COMPILED_TEXT_REGEX.search(sel_clean):
|
|
100
|
+
match = COMPILED_TEXT_REGEX.search(sel_clean).group(0)
|
|
101
|
+
return True, f"Element selector contains dangerous keyword: '{match}' in '{sel_clean}'"
|
|
89
102
|
|
|
90
103
|
# Specific dangerous action types
|
|
91
104
|
if action_type.lower() in ("drop_database", "wipe_environment", "execute_payment"):
|
package/tools/setup_project.py
CHANGED
|
@@ -175,7 +175,10 @@ def ensure_project_ai_guide(target_dir, stack_info):
|
|
|
175
175
|
|
|
176
176
|
|
|
177
177
|
def install_git_pre_push_hook(target_dir):
|
|
178
|
-
"""
|
|
178
|
+
"""
|
|
179
|
+
Installs or appends an automated pre-push hook into target project's .git repository.
|
|
180
|
+
Safely backs up any existing pre-push hook (e.g. Husky, Lefthook) to prevent side-effects.
|
|
181
|
+
"""
|
|
179
182
|
git_dir = os.path.join(target_dir, ".git")
|
|
180
183
|
if not os.path.isdir(git_dir):
|
|
181
184
|
print(" [SKIP] Not a Git repository root (.git folder not found). Skipped Git Hook installation.")
|
|
@@ -185,37 +188,64 @@ def install_git_pre_push_hook(target_dir):
|
|
|
185
188
|
os.makedirs(hooks_dir, exist_ok=True)
|
|
186
189
|
hook_file = os.path.join(hooks_dir, "pre-push")
|
|
187
190
|
|
|
191
|
+
# Resolve pinned package version
|
|
192
|
+
manifest_file = os.path.join(HUB_ROOT, "skills_manifest.json")
|
|
193
|
+
pkg_ver = "2.2.2"
|
|
194
|
+
if os.path.exists(manifest_file):
|
|
195
|
+
try:
|
|
196
|
+
with open(manifest_file, "r", encoding="utf-8") as f:
|
|
197
|
+
pkg_ver = json.load(f).get("version", pkg_ver)
|
|
198
|
+
except Exception:
|
|
199
|
+
pass
|
|
200
|
+
|
|
188
201
|
verify_all_script = os.path.join(SCRIPT_DIR, "verify_all.py").replace("\\", "/")
|
|
189
202
|
analyze_script = os.path.join(SCRIPT_DIR, "analyze_project.py").replace("\\", "/")
|
|
190
203
|
|
|
191
|
-
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
echo "\\n[AI-GUARD] Running Pre-Push Verification (8 Quality Gates DoD)..."
|
|
204
|
+
hook_snippet = f"""
|
|
205
|
+
# --- Synapse-Hub Pre-Push Quality Gate (v{pkg_ver}) ---
|
|
206
|
+
echo "\\n[AI-GUARD] Running Pre-Push Verification (Quality Gates DoD)..."
|
|
196
207
|
if command -v npx >/dev/null 2>&1; then
|
|
197
|
-
npx synapse-hub verify
|
|
208
|
+
npx synapse-hub@{pkg_ver} verify || npx synapse-hub verify
|
|
198
209
|
elif [ -f "{verify_all_script}" ]; then
|
|
199
210
|
python3 "{verify_all_script}" || python "{verify_all_script}"
|
|
200
211
|
fi
|
|
201
212
|
if [ $? -ne 0 ]; then
|
|
202
213
|
echo "\\n[AI-GUARD - REJECTED] Git Push aborted: Quality Gates verification failed!"
|
|
203
|
-
echo "Please fix defects or run 'npx synapse-hub verify' locally before pushing.\\n"
|
|
214
|
+
echo "Please fix defects or run 'npx synapse-hub@{pkg_ver} verify' locally before pushing.\\n"
|
|
204
215
|
exit 1
|
|
205
216
|
fi
|
|
206
217
|
|
|
207
|
-
echo "[AI-GUARD - PASSED]
|
|
218
|
+
echo "[AI-GUARD - PASSED] Quality Gates satisfied. Syncing project health report..."
|
|
208
219
|
if command -v npx >/dev/null 2>&1; then
|
|
209
|
-
npx synapse-hub analyze
|
|
220
|
+
npx synapse-hub@{pkg_ver} analyze || npx synapse-hub analyze
|
|
210
221
|
elif [ -f "{analyze_script}" ]; then
|
|
211
222
|
python3 "{analyze_script}" || python "{analyze_script}"
|
|
212
223
|
fi
|
|
213
224
|
echo "[AI-GUARD - READY] Pushing to remote...\\n"
|
|
214
|
-
|
|
225
|
+
# --- End Synapse-Hub Quality Gate ---
|
|
215
226
|
"""
|
|
227
|
+
|
|
216
228
|
try:
|
|
229
|
+
if os.path.exists(hook_file):
|
|
230
|
+
with open(hook_file, "r", encoding="utf-8", errors="ignore") as f:
|
|
231
|
+
existing_content = f.read()
|
|
232
|
+
|
|
233
|
+
if "Synapse-Hub" in existing_content or "synapse-hub" in existing_content:
|
|
234
|
+
print(f" {CYAN}[SKIP] Pre-Push Git Hook already present:{RESET} {hook_file}")
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
# Safely backup existing hook (e.g. Husky, Lefthook)
|
|
238
|
+
backup_file = f"{hook_file}.backup"
|
|
239
|
+
shutil.copy2(hook_file, backup_file)
|
|
240
|
+
print(f" {YELLOW}[BACKUP] Existing pre-push hook backed up to:{RESET} {backup_file}")
|
|
241
|
+
|
|
242
|
+
# Append non-destructively
|
|
243
|
+
final_content = existing_content.rstrip() + "\n" + hook_snippet
|
|
244
|
+
else:
|
|
245
|
+
final_content = "#!/bin/sh\n" + hook_snippet
|
|
246
|
+
|
|
217
247
|
with open(hook_file, "w", encoding="utf-8", newline="\n") as f:
|
|
218
|
-
f.write(
|
|
248
|
+
f.write(final_content)
|
|
219
249
|
|
|
220
250
|
# Attempt to make executable on Unix-like environments
|
|
221
251
|
try:
|
|
@@ -223,7 +253,7 @@ exit 0
|
|
|
223
253
|
except Exception:
|
|
224
254
|
pass
|
|
225
255
|
|
|
226
|
-
print(f" {GREEN}[OK]
|
|
256
|
+
print(f" {GREEN}[OK] Configured automated Pre-Push Git Hook:{RESET} {hook_file} (pinned to v{pkg_ver})")
|
|
227
257
|
return True
|
|
228
258
|
except Exception as e:
|
|
229
259
|
print(f" ⚠️ Could not install Git hook: {e}")
|
package/tools/skillspector.py
CHANGED
|
@@ -165,7 +165,7 @@ PROHIBITION_PREFIX = re.compile(
|
|
|
165
165
|
)
|
|
166
166
|
|
|
167
167
|
ALERT_BLOCK_PREFIX = re.compile(
|
|
168
|
-
r"^\s*>\s*\[!(?:WARNING|CAUTION|DANGER|ALERT
|
|
168
|
+
r"^\s*>\s*\[!(?:WARNING|CAUTION|DANGER|ALERT)\]",
|
|
169
169
|
re.IGNORECASE | re.MULTILINE,
|
|
170
170
|
)
|
|
171
171
|
|
|
@@ -189,23 +189,20 @@ def is_in_prohibition_context(content: str, start: int, end: int) -> bool:
|
|
|
189
189
|
if PROHIBITION_PREFIX.search(current_line):
|
|
190
190
|
return True
|
|
191
191
|
|
|
192
|
-
# 2. Markdown Caution/Warning Callout Block
|
|
193
|
-
|
|
192
|
+
# 2. Markdown Caution/Warning Callout Block with explicit negative imperative
|
|
193
|
+
has_alert = any(ALERT_BLOCK_PREFIX.search(line) for line in lines[-5:])
|
|
194
|
+
if has_alert and any(re.search(r'\b(never|do not|don[\'’]t|must not|forbidden|prohibited|avoid)\b', line, re.IGNORECASE) for line in lines[-5:]):
|
|
194
195
|
return True
|
|
195
196
|
|
|
196
197
|
# 3. Defensive Guideline headings ("Anti-Pattern Blacklist", "Forbidden actions")
|
|
197
198
|
preceding_lower = preceding_text.lower()
|
|
198
199
|
defensive_markers = (
|
|
199
|
-
"anti-pattern",
|
|
200
|
+
"anti-pattern blacklist",
|
|
200
201
|
"mistake / anti-pattern",
|
|
201
202
|
"strictly forbidden",
|
|
202
203
|
"never use",
|
|
203
204
|
"mandatory avoidance",
|
|
204
205
|
"forbidden actions",
|
|
205
|
-
"prohibited",
|
|
206
|
-
"blacklist",
|
|
207
|
-
"threat model",
|
|
208
|
-
"vulnerability check",
|
|
209
206
|
"zero-token error blacklist",
|
|
210
207
|
)
|
|
211
208
|
if any(marker in preceding_lower for marker in defensive_markers):
|
|
@@ -927,6 +924,9 @@ def scan_target(target_path: str) -> ScanReport:
|
|
|
927
924
|
pass
|
|
928
925
|
|
|
929
926
|
path_obj = Path(target_path).resolve()
|
|
927
|
+
if not path_obj.exists():
|
|
928
|
+
raise FileNotFoundError(f"Target path does not exist: {target_path}")
|
|
929
|
+
|
|
930
930
|
all_findings: List[Finding] = []
|
|
931
931
|
scanned_count = 0
|
|
932
932
|
|
package/tools/verify_all.py
CHANGED
|
@@ -237,43 +237,42 @@ def gate_2_path_portability(repo_root, mode="AUTO"):
|
|
|
237
237
|
|
|
238
238
|
|
|
239
239
|
def gate_3_clean_pom(repo_root, mode="AUTO"):
|
|
240
|
-
print("\n[GATE 3] Checking Clean POM & Flat Architecture in Scripts
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
240
|
+
print("\n[GATE 3] Checking Clean POM & Flat Architecture in Test Scripts...")
|
|
241
|
+
effective_mode = detect_repo_mode(repo_root) if mode == "AUTO" else mode
|
|
242
|
+
if effective_mode == "HUB":
|
|
243
|
+
print("[PASS] Hub Mode: Repository contains QA Hub assets; Clean POM applies to target test suites.")
|
|
244
|
+
return True
|
|
245
|
+
|
|
246
|
+
test_dirs = ["Scripts", "tests", "test", "e2e", "specs", "cypress"]
|
|
247
|
+
test_files = []
|
|
248
|
+
for td in test_dirs:
|
|
249
|
+
d = os.path.join(repo_root, td)
|
|
250
|
+
if os.path.isdir(d):
|
|
251
|
+
for ext in ("*.groovy", "*.ts", "*.js", "*.py", "*.java"):
|
|
252
|
+
test_files.extend(glob.glob(os.path.join(d, "**", ext), recursive=True))
|
|
253
|
+
|
|
254
|
+
if not test_files:
|
|
255
|
+
print("[SKIP] No test scripts present in candidate directories (Scripts/, tests/, e2e/).")
|
|
256
|
+
return True
|
|
255
257
|
|
|
256
258
|
violations = []
|
|
257
|
-
for
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
violations.append((rel_path, line_no, "Raw XPath"))
|
|
272
|
-
except Exception:
|
|
273
|
-
pass
|
|
259
|
+
for full_path in sorted(set(test_files)):
|
|
260
|
+
rel_path = os.path.relpath(full_path, repo_root)
|
|
261
|
+
try:
|
|
262
|
+
with open(full_path, "r", encoding="utf-8", errors="ignore") as fp:
|
|
263
|
+
for line_no, line in enumerate(fp, 1):
|
|
264
|
+
stripped = line.strip()
|
|
265
|
+
if stripped.startswith("//") or stripped.startswith("/*") or stripped.startswith("#"):
|
|
266
|
+
continue
|
|
267
|
+
if "findTestObject" in line:
|
|
268
|
+
violations.append((rel_path, line_no, "Raw findTestObject"))
|
|
269
|
+
if "//" in line and ("//table" in line or "//input" in line or "//button" in line):
|
|
270
|
+
violations.append((rel_path, line_no, "Raw XPath"))
|
|
271
|
+
except Exception:
|
|
272
|
+
pass
|
|
274
273
|
|
|
275
274
|
if violations:
|
|
276
|
-
print(f"[FAIL] {len(violations)} Clean POM violations in
|
|
275
|
+
print(f"[FAIL] {len(violations)} Clean POM violations in test scripts:")
|
|
277
276
|
for rel_path, line_no, reason in violations:
|
|
278
277
|
print(f" - {rel_path}:{line_no} ({reason})")
|
|
279
278
|
return False
|
|
@@ -404,26 +403,35 @@ def gate_6_credential_security(repo_root, mode="AUTO"):
|
|
|
404
403
|
|
|
405
404
|
def gate_7_infer_linter(repo_root, mode="AUTO"):
|
|
406
405
|
print("\n[GATE 7] Running Meta Infer-Style Static Defect & Fake-Pass Sentinel...")
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
406
|
+
effective_mode = detect_repo_mode(repo_root) if mode == "AUTO" else mode
|
|
407
|
+
if effective_mode == "HUB":
|
|
408
|
+
print("[PASS] Hub Mode: Static Defect Sentinel active for target projects.")
|
|
409
|
+
return True
|
|
410
|
+
|
|
411
|
+
test_dirs = ["Scripts", "tests", "test", "e2e", "specs", "cypress"]
|
|
412
|
+
has_tests = False
|
|
413
|
+
target_scan_dir = None
|
|
414
|
+
for td in test_dirs:
|
|
415
|
+
d = os.path.join(repo_root, td)
|
|
416
|
+
if os.path.isdir(d):
|
|
417
|
+
for ext in ("*.groovy", "*.ts", "*.js", "*.py", "*.java"):
|
|
418
|
+
if glob.glob(os.path.join(d, "**", ext), recursive=True):
|
|
419
|
+
has_tests = True
|
|
420
|
+
target_scan_dir = d
|
|
421
|
+
break
|
|
422
|
+
if has_tests:
|
|
423
|
+
break
|
|
424
|
+
|
|
425
|
+
if not has_tests:
|
|
426
|
+
print("[SKIP] No test scripts (Groovy/TS/JS/Py) present in candidate test directories.")
|
|
427
|
+
return True
|
|
420
428
|
|
|
421
429
|
infer_script = os.path.join(SCRIPT_DIR, "infer_linter.py")
|
|
422
430
|
if not os.path.exists(infer_script):
|
|
423
431
|
infer_script = os.path.join(repo_root, "tools", "infer_linter.py")
|
|
424
432
|
|
|
425
433
|
if os.path.exists(infer_script):
|
|
426
|
-
res = subprocess.run([sys.executable, infer_script, "--dir",
|
|
434
|
+
res = subprocess.run([sys.executable, infer_script, "--dir", target_scan_dir], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
427
435
|
if res.returncode != 0:
|
|
428
436
|
print("[FAIL] Static Defect Sentinel detected critical issues in test scripts:")
|
|
429
437
|
print(res.stdout)
|