Skip to content

Commit 4c4a2d4

Browse files
author
ChidcGithub
committed
v0.7.0-beta2: Gesture stability improvements & point-to-select feature
- Added gesture stability filter (5 consecutive frames) - Raised confidence threshold to 0.7 - Added 300ms cooldown between gesture changes - Fast reset when hand leaves frame - Point-to-select feature with 2s hover progress - Fixed undefined GestureType references - Expanded security patterns (25+ secret detection) - ZIP path validation improvements - Title updated to PyVisAST
1 parent 67b90ee commit 4c4a2d4

15 files changed

Lines changed: 729 additions & 79 deletions

File tree

README.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# PyVizAST
22

3-
[![Version](https://img.shields.io/badge/Version-0.7.0--beta-orange.svg)](https://github.com/ChidcGithub/PyVizAST)
3+
[![Version](https://img.shields.io/badge/Version-0.7.0--beta2-orange.svg)](https://github.com/ChidcGithub/PyVizAST)
44
[![Python](https://img.shields.io/badge/Python-3.8%2B-brightgreen.svg)](https://www.python.org/)
55
[![License](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](LICENSE)
66
[![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey.svg)](https://github.com/ChidcGithub/PyVizAST)
@@ -199,6 +199,52 @@ Contributions are welcome. Please submit pull requests to the main repository.
199199

200200
<summary>Version History</summary>
201201

202+
<details>
203+
<summary>v0.7.0-beta2 (2026-03-12)</summary>
204+
205+
**Gesture Control Improvements**
206+
207+
**Stability Enhancements:**
208+
- Added gesture stability filter: requires 5 consecutive frames with same gesture
209+
- Raised confidence threshold from 0.5 to 0.7
210+
- Added 300ms cooldown period between gesture changes
211+
- Fast reset when hand leaves frame (immediate instead of waiting 5 frames)
212+
- Position tracking continues during cooldown for smooth UX
213+
214+
**Simplified Gesture Set (4 core gestures + pinch):**
215+
- **Thumb Up**: Zoom in
216+
- **Thumb Down**: Zoom out
217+
- **Closed Fist**: Pan mode
218+
- **Open Palm**: Reset view
219+
- **Victory (V sign)**: Select node
220+
- **Pointing Up**: Point-to-select with hover progress
221+
- **Two Hands Pinch**: Pinch to zoom
222+
223+
**Point-to-Select Feature:**
224+
- Virtual cursor appears on AST graph when pointing
225+
- Hover over a node for 2 seconds to auto-select
226+
- Progress ring animation shows selection countdown
227+
- X-axis mirrored to match video preview
228+
229+
**Bug Fixes:**
230+
- Fixed undefined GestureType references (POINTING_UP, VICTORY)
231+
- Fixed callback cleanup on component unmount
232+
- Fixed cooldown visual feedback timing
233+
234+
**Files Modified:**
235+
- `frontend/src/utils/GestureService.js` - Stability filtering, cooldown, pointing direction
236+
- `frontend/src/components/GestureControl.js` - Removed unused icons, added pointing callback
237+
- `frontend/src/components/GestureControl.css` - Cooldown styling
238+
- `frontend/src/utils/logger.js` - Browser safety checks
239+
- `frontend/src/components/ASTVisualizer.js` - Pointing cursor, hover detection, progress ring
240+
- `frontend/src/components/components.css` - Pointing cursor styling
241+
- `frontend/src/App.js` - Pointing direction handling
242+
- `backend/analyzers/security.py` - Expanded secret detection (25+ patterns)
243+
- `backend/project_analyzer/scanner.py` - ZIP path validation
244+
- `frontend/public/index.html` - Title updated to "PyVisAST"
245+
246+
</details>
247+
202248
<details>
203249
<summary>v0.7.0-beta (2026-03-11)</summary>
204250

backend/analyzers/security.py

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,41 @@ class SecurityScanner:
3838
# Sensitive word patterns (for detecting hardcoded secrets)
3939
# Updated to handle escaped quotes in string values
4040
SENSITIVE_PATTERNS = [
41+
# Password patterns
4142
(r'(?i)(password|passwd|pwd)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]', 'password'),
42-
(r'(?i)(api_key|apikey|api_secret)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]', 'API key'),
43-
(r'(?i)(secret|secret_key)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]', 'secret key'),
44-
(r'(?i)(token|auth_token|access_token)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]', 'token'),
45-
(r'(?i)(private_key|privatekey)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]', 'private key'),
46-
(r'(?i)(aws_access_key|aws_secret)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]', 'AWS credential'),
47-
(r'(?i)(database_url|db_password)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]', 'database credential'),
43+
(r'(?i)(password|passwd|pwd)\s*=\s*[\'"][^\'"]{8,}[\'"]', 'password'), # Longer passwords
44+
45+
# API keys and secrets
46+
(r'(?i)(api_key|apikey|api_secret|api-key)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]', 'API key'),
47+
(r'(?i)api[_-]?key[_-]?\w*\s*=\s*[\'"][a-zA-Z0-9_\-]{16,}[\'"]', 'API key'), # API_KEY_XXX pattern
48+
(r'(?i)(secret|secret_key|secretkey)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]', 'secret key'),
49+
(r'(?i)secret[_-]?key[_-]?\w*\s*=\s*[\'"][a-zA-Z0-9_\-]{16,}[\'"]', 'secret key'),
50+
51+
# Token patterns
52+
(r'(?i)(token|auth_token|access_token|bearer|jwt)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]', 'token'),
53+
(r'(?i)(token|auth_token|access_token)\s*=\s*[\'"][a-zA-Z0-9_\-\.]{20,}[\'"]', 'token'),
54+
(r'(?i)jwt[_-]?secret\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]', 'JWT secret'),
55+
56+
# Private keys
57+
(r'(?i)(private_key|privatekey|private-key)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]', 'private key'),
58+
(r'(?i)(private_key|privatekey)\s*=\s*[\'"][\'"]-----BEGIN', 'private key'), # PEM format
59+
60+
# Cloud provider credentials
61+
(r'(?i)(aws_access_key|aws_secret|aws_key)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]', 'AWS credential'),
62+
(r'(?i)(gcp_key|gcp_secret|google_api_key)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]', 'GCP credential'),
63+
(r'(?i)(azure_key|azure_secret|azure_connection_string)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]', 'Azure credential'),
64+
65+
# Database credentials
66+
(r'(?i)(database_url|db_password|db_user|db_host)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]', 'database credential'),
67+
(r'(?i)(mongodb_uri|mongo_url|postgres_url|mysql_url)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]', 'database URL'),
68+
69+
# OAuth and authentication
70+
(r'(?i)(oauth_token|oauth_secret|client_secret)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]', 'OAuth credential'),
71+
(r'(?i)(client_id|client_secret)\s*=\s*[\'"][a-zA-Z0-9_\-]{16,}[\'"]', 'OAuth credential'),
72+
73+
# Encryption keys
74+
(r'(?i)(encryption_key|encrypt_key|cipher_key)\s*=\s*[\'"](?:[^\'"\\]|\\.)*[\'"]', 'encryption key'),
75+
(r'(?i)(salt|iv)\s*=\s*[\'"][a-zA-Z0-9_\-]{8,}[\'"]', 'encryption salt/IV'),
4876
]
4977

5078
# SQL injection risk patterns
@@ -286,14 +314,19 @@ def _get_func_full_name(self, node: ast.AST) -> str:
286314

287315
def _check_command_injection(self, tree: ast.AST):
288316
"""Check for command injection risks"""
289-
os_functions = {'system', 'popen', 'spawn', 'call', 'run'}
290-
subprocess_functions = {'call', 'run', 'Popen', 'check_output'}
317+
# Extended list of dangerous os functions
318+
os_functions = {'system', 'popen', 'spawn', 'spawnl', 'spawnle', 'spawnlp',
319+
'spawnlpe', 'spawnv', 'spawnve', 'spawnvp', 'spawnvpe',
320+
'call', 'run', 'startfile'}
321+
subprocess_functions = {'call', 'run', 'Popen', 'check_output', 'check_call', 'getoutput', 'getstatusoutput'}
322+
# Legacy/deprecated modules (Python 2 compatibility layer in Python 3)
323+
legacy_modules = {'commands', 'popen2', 'popen3', 'popen4'}
291324

292325
for node in ast.walk(tree):
293326
if isinstance(node, ast.Call):
294327
func_full_name = self._get_func_full_name(node.func)
295328

296-
# os.system, os.popen
329+
# os.system, os.popen, os.spawn*, etc.
297330
if func_full_name.startswith('os.'):
298331
# Ensure node.func is ast.Attribute type before accessing attr
299332
if isinstance(node.func, ast.Attribute) and node.func.attr in os_functions:
@@ -336,6 +369,43 @@ def _check_command_injection(self, tree: ast.AST):
336369
lineno=node.lineno,
337370
suggestion="Use shell=False and pass argument list"
338371
))
372+
373+
# Legacy modules like commands.getoutput
374+
elif func_full_name.startswith('commands.'):
375+
self.issues.append(CodeIssue(
376+
id=self._generate_issue_id("legacy_command"),
377+
type="security",
378+
severity=SeverityLevel.ERROR,
379+
message=f"Legacy module 'commands' is insecure and removed in Python 3",
380+
lineno=node.lineno,
381+
suggestion="Use subprocess.run() with shell=False"
382+
))
383+
384+
# popen2, popen3, popen4 modules
385+
elif any(func_full_name.startswith(f'{mod}.') for mod in legacy_modules):
386+
self.issues.append(CodeIssue(
387+
id=self._generate_issue_id("popen_legacy"),
388+
type="security",
389+
severity=SeverityLevel.ERROR,
390+
message=f"Legacy module '{func_full_name.split('.')[0]}' is deprecated and insecure",
391+
lineno=node.lineno,
392+
suggestion="Use subprocess.Popen() instead"
393+
))
394+
395+
# eval() and exec() with potentially user-controlled input
396+
elif isinstance(node.func, ast.Name) and node.func.id in ('eval', 'exec'):
397+
if node.args:
398+
arg = node.args[0]
399+
# Check if it's not a constant (i.e., potentially dynamic)
400+
if not isinstance(arg, ast.Constant):
401+
self.issues.append(CodeIssue(
402+
id=self._generate_issue_id("code_injection"),
403+
type="security",
404+
severity=SeverityLevel.ERROR,
405+
message=f"Using {node.func.id}() with dynamic input is a code injection risk",
406+
lineno=node.lineno,
407+
suggestion="Avoid eval/exec with user input; use ast.literal_eval() for safe evaluation"
408+
))
339409

340410
def _check_path_traversal(self, tree: ast.AST):
341411
"""Check for path traversal risks"""

backend/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
app = FastAPI(
4242
title="PyVizAST API",
4343
description="Python AST Visualization and Static Analysis API",
44-
version="0.7.0-beta",
44+
version="0.7.0-beta2",
4545
docs_url="/docs",
4646
redoc_url="/redoc"
4747
)

backend/project_analyzer/scanner.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,31 @@ def scan_zip(self, zip_path: str, project_name: Optional[str] = None) -> Tuple[P
8888
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
8989
# Check for path traversal attacks before extracting
9090
for member in zip_ref.namelist():
91+
# Skip entries that are absolute paths (Unix or Windows style)
92+
if member.startswith('/') or (len(member) > 1 and member[1] == ':'):
93+
logger.warning(f"Skipping absolute path in ZIP: {member}")
94+
continue
95+
96+
# Skip entries with parent directory references
97+
if '..' in member.split(os.sep) or '..' in member.split('/'):
98+
logger.warning(f"Skipping path with parent reference in ZIP: {member}")
99+
continue
100+
91101
# Resolve the member path and check if it's within temp_dir
92102
member_path = os.path.realpath(os.path.join(temp_dir, member))
93-
if not member_path.startswith(os.path.realpath(temp_dir) + os.sep) and member_path != os.path.realpath(temp_dir):
103+
temp_dir_real = os.path.realpath(temp_dir)
104+
105+
# Check if the resolved path is within the target directory
106+
if not member_path.startswith(temp_dir_real + os.sep) and member_path != temp_dir_real:
94107
logger.warning(f"Skipping potentially malicious path in ZIP: {member}")
95108
continue
109+
110+
# Check if it's a symlink in the ZIP (suspicious)
111+
member_info = zip_ref.getinfo(member)
112+
if hasattr(member_info, 'external_attr') and (member_info.external_attr >> 28) == 0xA:
113+
logger.warning(f"Skipping symbolic link in ZIP: {member}")
114+
continue
115+
96116
# Extract individual member safely
97117
zip_ref.extract(member, temp_dir)
98118

frontend/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pyvizast-frontend",
3-
"version": "0.7.0-beta",
3+
"version": "0.7.0-beta2",
44
"author": "Chidc (github.com/chidcGithub)",
55
"private": true,
66
"dependencies": {

frontend/public/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
<meta charset="utf-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1" />
66
<meta name="theme-color" content="#1a1a2e" />
7-
<meta name="description" content="PyVizAST - Python AST可视化与静态分析器" />
8-
<title>PyVizAST - Python代码可视化分析器</title>
7+
<meta name="description" content="PyVisAST" />
8+
<title>PyVisAST</title>
99
<!-- 主字体源:Google Fonts -->
1010
<link rel="preconnect" href="https://fonts.googleapis.com">
1111
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

frontend/src/App.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import ProjectVisualization from './components/ProjectVisualization';
99
import LearnView from './components/LearnView';
1010
import ChallengeView from './components/ChallengeView';
1111
import GestureControl from './components/GestureControl';
12+
import { GestureType } from './utils/GestureService';
1213
import { analyzeCode, checkServerHealth, getApiBaseUrl } from './api';
1314
import { setupGlobalErrorHandlers } from './utils/logger';
1415
import './App.css';
@@ -492,14 +493,25 @@ function App() {
492493

493494
// Gesture control toggle
494495
const handleGestureToggle = useCallback(() => {
495-
setGestureEnabled(prev => !prev);
496+
setGestureEnabled(prev => {
497+
const newValue = !prev;
498+
// Clear pointing cursor when disabling gesture control
499+
if (!newValue && visualizerRef.current && visualizerRef.current.clearPointingCursor) {
500+
visualizerRef.current.clearPointingCursor();
501+
}
502+
return newValue;
503+
});
496504
}, []);
497505

498506
// Gesture callback - receives gesture data and forwards to visualizer
499507
const handleGesture = useCallback((gestureData) => {
500508
if (visualizerRef.current && visualizerRef.current.handleGesture) {
501509
visualizerRef.current.handleGesture(gestureData);
502510
}
511+
// Clear pointing cursor when gesture is not Pointing_Up
512+
if (gestureData.gesture !== GestureType.POINTING_UP && visualizerRef.current && visualizerRef.current.clearPointingCursor) {
513+
visualizerRef.current.clearPointingCursor();
514+
}
503515
}, []);
504516

505517
// Two hands gesture callback - for pinch zoom
@@ -509,6 +521,13 @@ function App() {
509521
}
510522
}, []);
511523

524+
// Pointing direction callback - for gesture-based node selection
525+
const handlePointingDirection = useCallback((pointingData) => {
526+
if (visualizerRef.current && visualizerRef.current.handlePointingDirection) {
527+
visualizerRef.current.handlePointingDirection(pointingData);
528+
}
529+
}, []);
530+
512531
// Toast notification helper
513532
const showToast = useCallback((message, type = 'success') => {
514533
setToast({ message, type });
@@ -967,6 +986,7 @@ function App() {
967986
enabled={gestureEnabled}
968987
onGesture={handleGesture}
969988
onTwoHands={handleTwoHandsGesture}
989+
onPointingDirection={handlePointingDirection}
970990
theme={theme}
971991
showGuide={true}
972992
compact={false}

0 commit comments

Comments
 (0)