Compare commits

...
81 Commits
Author SHA1 Message Date
87owo 8c1112b517 Add files via upload 2026-09-22 14:32:16 +08:00
87owo 8c2085d851 Update PYAS Security interface image link
Updated the image link for the PYAS Security interface in the README.
2026-09-22 14:30:21 +08:00
87owo 5b9fd96dfe Fix spacing in command-line argument examples 2026-09-22 14:18:42 +08:00
87owo 9a69acece8 Fix formatting of project description in README 2026-09-22 14:16:26 +08:00
87owo 4367226be8 Enhance README with badges and formatting
Updated the README to include badges and improved formatting.
2026-09-22 14:15:48 +08:00
87owo be2088f6f3 Fix README links and Python API client reference
Updated links and file references in the README.
2026-09-22 14:15:07 +08:00
87owo 9de779ba3f Revise README with project details and installation info
Updated project description, added badges, and improved overview section. Included installation instructions and clarified optional dependencies.
2026-09-22 14:02:19 +08:00
87owo 6cb8b801c7 Add files via upload 2026-09-22 13:33:35 +08:00
87owo b89e91cdb8 Add files via upload 2026-09-22 10:09:21 +08:00
87owo 50b8378e0a Add files via upload 2026-09-22 10:08:15 +08:00
87owo dd108b5c8a Add files via upload 2026-09-22 02:26:15 +08:00
87owo be1d86ca66 Add files via upload 2026-09-22 01:51:17 +08:00
87owo 2ef0fe145f Delete Engine/Properties/train_user.py 2026-09-22 01:50:44 +08:00
87owo af3a92c7d2 Add files via upload 2026-09-20 16:27:08 +08:00
87owo 411e6dba8f Add files via upload 2026-09-20 16:25:52 +08:00
87owo 9db8d33df4 Add files via upload 2026-09-15 00:12:12 +08:00
87owo 814baadbdf Add files via upload 2026-09-15 00:09:47 +08:00
87owo bc21628387 Add files via upload 2026-09-15 00:07:58 +08:00
87owo e7634c3c37 Add files via upload 2026-09-05 09:22:22 +08:00
87owo 05e952c5ee Add files via upload 2026-09-04 13:11:56 +08:00
87owo 8a0209a99a Add files via upload 2026-09-04 13:10:39 +08:00
87owo 65bc5c7c2a Add files via upload 2026-09-04 09:20:51 +08:00
87owo 07c64042be Add files via upload 2026-08-24 22:39:54 +08:00
87owo 22c04063cf Add files via upload 2026-08-23 10:33:53 +08:00
87owo 68a7ab657a Update README.md 2026-08-21 16:33:16 +08:00
87owo 630dc67058 Add files via upload 2026-08-21 14:19:12 +08:00
87owo 591507512c Delete Plugins/Rules/Report.json 2026-08-21 13:33:13 +08:00
87owo ff49cafc74 Add files via upload 2026-08-21 13:32:08 +08:00
87owo b077ba06bd Add files via upload 2026-08-19 17:20:02 +08:00
87owo a2b66aec64 Add files via upload 2026-08-14 12:43:24 +08:00
87owo 4b692b0c87 Add files via upload 2026-08-11 13:17:24 +08:00
87owo 99d1b20d0f Add files via upload 2026-08-09 19:52:15 +08:00
87owo fae5edb57b Add files via upload 2026-08-09 19:50:38 +08:00
87owo 6db91483a0 Add files via upload 2026-08-09 19:36:22 +08:00
87owo 90383f3d09 Add files via upload 2026-08-04 22:31:09 +08:00
87owo 966a5a3592 Add files via upload 2026-08-04 22:29:20 +08:00
87owo 48e14cd143 Add files via upload 2026-07-28 12:29:39 +08:00
87owo 4264f50ba3 Add files via upload 2026-07-28 12:12:10 +08:00
87owo 887e0718d0 Add files via upload 2026-07-28 12:10:36 +08:00
87owo 4870a5c480 Add files via upload 2026-07-28 12:09:57 +08:00
87owo e8cadf0537 Add files via upload 2026-07-27 17:30:57 +08:00
87owo f5124e0ef2 Add files via upload 2026-07-25 20:33:44 +08:00
87owo da9a520632 Add files via upload 2026-07-25 20:12:57 +08:00
87owo 81bebeda63 Add files via upload 2026-07-25 00:08:11 +08:00
87owo 4df1432be1 Add files via upload 2026-07-24 23:25:19 +08:00
87owo e699be6b2e Update Rules_Driver_P1.json 2026-07-24 23:22:26 +08:00
87owo e8dda40e7f Add files via upload 2026-07-22 17:21:30 +08:00
87owo 5b4cd6e879 Add files via upload 2026-07-22 16:55:36 +08:00
87owo 316b061d34 Add files via upload 2026-07-22 13:22:34 +08:00
87owo 6ff27679e7 Add files via upload 2026-07-22 13:17:06 +08:00
87owo 8f3adbc870 Add files via upload 2026-07-22 13:13:08 +08:00
87owo 5798a35456 Add files via upload 2026-07-18 18:08:03 +08:00
87owo a8a2060978 Add files via upload 2026-07-18 17:38:02 +08:00
87owo 7cf2763511 Add files via upload 2026-07-18 17:34:32 +08:00
87owo fc59619518 Revise Driver Signature notice in LICENSE
Updated the language in the Driver Signature section for clarity.
2026-07-18 17:17:58 +08:00
87owo d01cfd2dd6 Update LICENSE.txt with new date and driver notice
Updated the last modified date in the license agreement and added a new section regarding driver signature notice.
2026-07-18 17:03:00 +08:00
87owo 9563a4806a Add files via upload 2026-07-17 17:55:26 +08:00
87owo fa1be109e3 Add files via upload 2026-07-17 17:41:26 +08:00
87owo 89a5a5928c Add files via upload 2026-07-17 11:36:43 +08:00
87owo 7830378f23 Add files via upload 2026-07-17 09:42:55 +08:00
87owo 043f616469 Add files via upload 2026-07-17 09:40:34 +08:00
87owo 3bf631313a Add files via upload 2026-07-17 09:38:11 +08:00
87owo a416e2d4a8 Add files via upload 2026-07-16 16:21:57 +08:00
87owo 53218cf1ce Add files via upload 2026-07-15 22:06:33 +08:00
87owo c5f0c8ed47 Add files via upload 2026-07-15 21:52:37 +08:00
87owo 05e1c45adb Add files via upload 2026-07-15 21:20:11 +08:00
87owo e3787523c7 Add files via upload 2026-07-15 21:19:15 +08:00
87owo 1c4e908b1b Add files via upload 2026-07-14 04:54:50 +08:00
87owo 98b60b9833 Add files via upload 2026-07-14 04:54:15 +08:00
87owo 71f8232ef0 Delete Plugins/Rules directory 2026-07-14 04:54:03 +08:00
87owo 0e4de407e3 Add files via upload 2026-07-14 04:53:34 +08:00
87owo 802d732b3f Add files via upload 2026-07-13 19:36:21 +08:00
87owo 373897f579 Add files via upload 2026-07-12 12:01:26 +08:00
87owo 3ef72917c8 Add files via upload 2026-07-12 12:00:57 +08:00
87owo 0d27125453 Add files via upload 2026-07-12 11:50:19 +08:00
87owo 82c519b7d9 Add files via upload 2026-07-12 11:49:34 +08:00
87owo 3c57a8a53c Add files via upload 2026-07-11 21:21:12 +08:00
87owo 727ba6ccbf Add files via upload 2026-07-11 15:16:35 +08:00
87owo b32f1df9ec Add files via upload 2026-07-10 00:23:20 +08:00
87owo 76b9afc6ec Add files via upload 2026-07-10 00:22:49 +08:00
87owo 9fbf7baf76 Add files via upload 2026-07-10 00:12:16 +08:00
25 changed files with 19822 additions and 6914 deletions

No files matched your search

File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+1101 -744
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+1156 -798
View File
File diff suppressed because it is too large. Load diff
+309
View File
@@ -0,0 +1,309 @@
import os, sys, time, json, zlib, array, onnxmltools, multiprocessing
import numpy as np
import pandas as pd
import lightgbm as lgb
import scipy.sparse as sp
from collections import defaultdict
from onnxmltools.convert.common.data_types import FloatTensorType
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, roc_auc_score
try:
import orjson
JSON_LOADS = orjson.loads
except ImportError:
JSON_LOADS = json.loads
####################################################################################################
JSONL_PATH = "pe_features.jsonl"
MODEL_FILE = "Pefile_General_E1.txt"
ONNX_FILE = "Pefile_General_E1.onnx"
FEATURE_FILE = "features.json"
TEST_SIZE = 0.0001
RANDOM_SEED = 42
DLL_HASH_DIM = 512
API_HASH_DIM = 4096
EXCLUDE_FEATURES = {
'label', 'filehash',
}
LGBM_PARAMS = {
'objective': 'binary',
'metric': ['binary_logloss', 'auc'],
'boosting_type': 'gbdt',
'num_leaves': 256,
'feature_fraction': 0.8,
'bagging_fraction': 1.0,
'bagging_freq': 5,
'verbose': -1,
'seed': RANDOM_SEED,
'n_jobs': -1,
'max_depth': -1,
'min_data_in_leaf': 50,
}
####################################################################################################
def analyze_schema(jsonl_path):
print("[*] Stage 1: Scanning global schema...")
base_keys = set()
num_lines = 0
with open(jsonl_path, 'rb') as f:
for line in f:
num_lines += 1
try:
data = JSON_LOADS(line)
base_data = data.get('Base')
if base_data:
base_keys.update(base_data.keys())
except Exception:
pass
base_schema = sorted([k for k in base_keys if k.lower().strip() not in EXCLUDE_FEATURES])
dll_schema = [f"DllHash_{i:03d}" for i in range(DLL_HASH_DIM)]
api_schema = [f"ApiHash_{i:04d}" for i in range(API_HASH_DIM)]
return base_schema, dll_schema, api_schema, num_lines
####################################################################################################
def load_data_efficiently(jsonl_path, base_schema, dll_schema, api_schema, num_lines):
print(f"[*] Stage 2: Building sparse feature matrix dynamically via Multiprocessing...")
final_features = base_schema + dll_schema + api_schema
num_features = len(final_features)
offset_dll = len(base_schema)
offset_api = offset_dll + DLL_HASH_DIM
file_size = os.path.getsize(jsonl_path)
workers = min(os.cpu_count() or 4, 16)
chunk_size = file_size // workers
tasks = []
for i in range(workers):
start = i * chunk_size
end = file_size if i == workers - 1 else (i + 1) * chunk_size
tasks.append((jsonl_path, start, end, base_schema, num_features, offset_dll, offset_api))
with multiprocessing.Pool(workers) as pool:
results = pool.map(_chunk_worker, tasks)
Xs = [r[0] for r in results if r[0].shape[0] > 0]
ys = [r[1] for r in results if r[1].shape[0] > 0]
X_final = sp.vstack(Xs, format='csr') if Xs else sp.csr_matrix((0, num_features), dtype=np.float32)
y_final = np.concatenate(ys) if ys else np.array([], dtype=np.int8)
return X_final, pd.Series(y_final), final_features
def _chunk_worker(args):
filepath, start_byte, end_byte, base_schema, num_features, offset_dll, offset_api = args
base_idx = {feat: i for i, feat in enumerate(base_schema)}
base_idx_get = base_idx.get
rows = array.array('i')
cols = array.array('i')
vals = array.array('f')
y_list = array.array('b')
with open(filepath, 'rb') as f:
if start_byte > 0:
f.seek(start_byte - 1)
if f.read(1) != b'\n':
f.seek(start_byte)
f.readline()
else:
f.seek(start_byte)
else:
f.seek(0)
row_idx = 0
while f.tell() < end_byte:
line = f.readline()
if not line:
break
try:
data = JSON_LOADS(line)
y_list.append(data.get('Label', 0))
base_data = data.get('Base')
if base_data:
for k, v in base_data.items():
idx = base_idx_get(k)
if idx is not None:
val = float(v)
if val != 0.0:
rows.append(row_idx)
cols.append(idx)
vals.append(val)
dll_data = data.get('DLLs')
if dll_data:
for d in dll_data:
idx = zlib.crc32(d.encode('utf-8', 'ignore')) % DLL_HASH_DIM
rows.append(row_idx)
cols.append(offset_dll + idx)
vals.append(1.0)
api_data = data.get('APIs')
if api_data:
for a in api_data:
idx = zlib.crc32(a.encode('utf-8', 'ignore')) % API_HASH_DIM
rows.append(row_idx)
cols.append(offset_api + idx)
vals.append(1.0)
row_idx += 1
except Exception:
continue
y_arr = np.frombuffer(y_list, dtype=np.int8)
np_rows = np.frombuffer(rows, dtype=np.int32)
np_cols = np.frombuffer(cols, dtype=np.int32)
np_vals = np.frombuffer(vals, dtype=np.float32)
X_local = sp.csr_matrix((np_vals, (np_rows, np_cols)), shape=(row_idx, num_features), dtype=np.float32)
return X_local, y_arr
####################################################################################################
def _calculate_dynamic_lr(current_iter: int) -> float:
return max(0.01, 0.1 - (current_iter // 80) * 0.01)
def train_process(X, y, feature_names):
print(f"[*] Dataset shape: {X.shape}")
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=TEST_SIZE, random_state=RANDOM_SEED, stratify=y)
n_pos = (y_train == 1).sum()
n_neg = (y_train == 0).sum()
base_weight = n_neg / n_pos if n_pos > 0 else 1.0
weight_ratio_target = 1
final_pos_weight = base_weight * weight_ratio_target
print(f"[*] Sample distribution: Safe={n_neg}, Malware={n_pos}")
print(f"[*] Weight correction: Base={base_weight:.4f}, Final={final_pos_weight:.4f}")
train_data = lgb.Dataset(X_train, label=y_train, feature_name=feature_names)
valid_data = lgb.Dataset(X_test, label=y_test, reference=train_data, feature_name=feature_names)
params = LGBM_PARAMS.copy()
params['scale_pos_weight'] = final_pos_weight
print("\n[*] Starting LightGBM training...")
start_time = time.time()
model = lgb.train(
params,
train_data,
valid_sets=[valid_data],
num_boost_round=500,
callbacks=[
lgb.log_evaluation(period=50),
lgb.reset_parameter(learning_rate=_calculate_dynamic_lr)
]
)
elapsed = time.time() - start_time
print(f"\n[+] Training complete, time elapsed {elapsed:.2f} seconds")
return model, X_test, y_test
####################################################################################################
def export_onnx_model(model, feature_names):
print(f"[*] Exporting ONNX model to {ONNX_FILE}...")
try:
initial_type = [('float_input', FloatTensorType([None, len(feature_names)]))]
onnx_model = onnxmltools.convert_lightgbm(model, initial_types=initial_type)
onnxmltools.utils.save_model(onnx_model, ONNX_FILE)
with open(FEATURE_FILE, 'w') as f:
json.dump(feature_names, f)
print(f"[+] ONNX export successful. Feature order saved to {FEATURE_FILE}")
except Exception as e:
print(f"[-] ONNX export failed: {e}")
def evaluate_and_save(model, X_test, y_test):
print("\n------------------- Evaluation Report -------------------\n")
y_prob = model.predict(X_test, num_iteration=model.best_iteration)
y_pred = (y_prob > 0.5).astype(int)
acc = accuracy_score(y_test, y_pred)
auc = roc_auc_score(y_test, y_prob)
tp = np.sum((y_test == 1) & (y_pred == 1))
fn = np.sum((y_test == 1) & (y_pred == 0))
tn = np.sum((y_test == 0) & (y_pred == 0))
fp = np.sum((y_test == 0) & (y_pred == 1))
total_malware = tp + fn
total_safe = tn + fp
detection_rate = (tp / total_malware * 100) if total_malware > 0 else 0.0
fpr = (fp / total_safe * 100) if total_safe > 0 else 0.0
print(f"Accuracy : {acc:.6f}")
print(f"ROC AUC : {auc:.6f}\n")
print(f"True Positive (Class 1) : {detection_rate:.3f}% ({tp}/{total_malware})")
print(f"False Positive (Class 0) : {fpr:.3f}% ({fp}/{total_safe})")
print("\n---------------------------------------------------------\n")
print("[*] Top 20 Features:")
feature_names = model.feature_name()
importance = model.feature_importance(importance_type='gain')
feature_imp = pd.DataFrame(sorted(zip(importance, feature_names), reverse=True), columns=['Value', 'Feature'])
print(feature_imp.head(20).to_string(index=False))
print("\n---------------------------------------------------------\n")
model.save_model(MODEL_FILE)
print(f"[+] Native model saved to: {os.path.abspath(MODEL_FILE)}")
export_onnx_model(model, feature_names)
####################################################################################################
if __name__ == "__main__":
multiprocessing.freeze_support()
print("\n---------------- PE Malware Trainer v4.0 ----------------\n")
if not os.path.exists(JSONL_PATH):
print(f"[-] Error: Feature file {JSONL_PATH} not found.")
sys.exit(1)
base_schema, dll_schema, api_schema, num_lines = analyze_schema(JSONL_PATH)
if num_lines == 0:
print("[-] Error: Feature file is empty.")
sys.exit(1)
print(f"[*] Detected Base features: {len(base_schema)}")
print(f"[*] Hashing DLL features: {len(dll_schema)}")
print(f"[*] Hashing API features: {len(api_schema)}")
X, y, final_features = load_data_efficiently(JSONL_PATH, base_schema, dll_schema, api_schema, num_lines)
try:
model, X_test, y_test = train_process(X, y, final_features)
evaluate_and_save(model, X_test, y_test)
except KeyboardInterrupt:
print("\n\n[-] Training aborted.")
except Exception as e:
print(f"\n[-] System exception: {e}")
+10 -11
View File
@@ -19,7 +19,7 @@ except ImportError:
####################################################################################################
JSONL_PATH = "pe_features.jsonl"
MODEL_FILE = "model.txt"
MODEL_FILE = "Pefile_General_S1.txt"
ONNX_FILE = "Pefile_General_S1.onnx"
FEATURE_FILE = "features.json"
@@ -58,15 +58,14 @@ def analyze_schema(jsonl_path):
for line in f:
num_lines += 1
if not base_keys:
try:
data = JSON_LOADS(line)
base_data = data.get('Base')
if base_data:
base_keys.update(base_data.keys())
try:
data = JSON_LOADS(line)
base_data = data.get('Base')
if base_data:
base_keys.update(base_data.keys())
except Exception:
pass
except Exception:
pass
base_schema = sorted([k for k in base_keys if k.lower().strip() not in EXCLUDE_FEATURES])
dll_schema = [f"DllHash_{i:03d}" for i in range(DLL_HASH_DIM)]
@@ -192,7 +191,7 @@ def train_process(X, y, feature_names):
n_neg = (y_train == 0).sum()
base_weight = n_neg / n_pos if n_pos > 0 else 1.0
weight_ratio_target = 0.005
weight_ratio_target = 0.01
final_pos_weight = base_weight * weight_ratio_target
print(f"[*] Sample distribution: Safe={n_neg}, Malware={n_pos}")
@@ -211,7 +210,7 @@ def train_process(X, y, feature_names):
params,
train_data,
valid_sets=[valid_data],
num_boost_round=500,
num_boost_round=350,
callbacks=[
lgb.log_evaluation(period=50),
lgb.reset_parameter(learning_rate=_calculate_dynamic_lr)
@@ -19,7 +19,7 @@ except ImportError:
####################################################################################################
JSONL_PATH = "pe_features.jsonl"
MODEL_FILE = "model.txt"
MODEL_FILE = "Pefile_General_T1.txt"
ONNX_FILE = "Pefile_General_T1.onnx"
FEATURE_FILE = "features.json"
@@ -36,7 +36,7 @@ LGBM_PARAMS = {
'objective': 'binary',
'metric': ['binary_logloss', 'auc'],
'boosting_type': 'gbdt',
'num_leaves': 128,
'num_leaves': 256,
'feature_fraction': 0.8,
'bagging_fraction': 1.0,
'bagging_freq': 5,
@@ -58,15 +58,14 @@ def analyze_schema(jsonl_path):
for line in f:
num_lines += 1
if not base_keys:
try:
data = JSON_LOADS(line)
base_data = data.get('Base')
if base_data:
base_keys.update(base_data.keys())
try:
data = JSON_LOADS(line)
base_data = data.get('Base')
if base_data:
base_keys.update(base_data.keys())
except Exception:
pass
except Exception:
pass
base_schema = sorted([k for k in base_keys if k.lower().strip() not in EXCLUDE_FEATURES])
dll_schema = [f"DllHash_{i:03d}" for i in range(DLL_HASH_DIM)]
@@ -192,7 +191,7 @@ def train_process(X, y, feature_names):
n_neg = (y_train == 0).sum()
base_weight = n_neg / n_pos if n_pos > 0 else 1.0
weight_ratio_target = 0.001
weight_ratio_target = 0.0001
final_pos_weight = base_weight * weight_ratio_target
print(f"[*] Sample distribution: Safe={n_neg}, Malware={n_pos}")
+135 -34
View File
@@ -1,4 +1,4 @@
* {
* {
box-sizing: border-box;
margin: 0;
padding: 0;
@@ -14,6 +14,7 @@
--bg-hover: rgba(0, 0, 0, 0.05);
--text-primary: rgb(29, 29, 31);
--text-secondary: rgb(134, 134, 139);
--text-on-accent: rgb(255, 255, 255);
--border-color: rgb(210, 210, 215);
--accent-color: rgb(0, 122, 255);
--accent-alpha: rgba(0, 122, 255, 0.1);
@@ -82,7 +83,7 @@ body {
}
.ctrl-btn:hover { background-color: var(--bg-hover); color: var(--text-primary); }
#close_button:hover { background-color: #ff3b30; color: white; }
#close_button:hover { background-color: #ff3b30; color: var(--text-on-accent); }
.theme-svg {
width: 16px;
@@ -258,7 +259,7 @@ body {
.primary-btn {
background-color: var(--accent-color);
color: white;
color: var(--text-on-accent);
border: none;
box-shadow: 0 2px 4px var(--accent-shadow);
}
@@ -324,6 +325,27 @@ body {
gap: 16px;
}
.item-icon {
width: 40px;
height: 40px;
flex: 0 0 40px;
stroke-width: 1.5;
color: var(--accent-color);
pointer-events: none;
}
.item-icon-compact {
transform: scale(0.9);
}
.item-icon-repair {
transform: translateX(2px) scale(1.1);
}
.item-icon ~ :is(.toggle-switch, .icon-action-btn) {
flex-shrink: 0;
}
.item-info {
flex: 1;
min-width: 0;
@@ -331,7 +353,8 @@ body {
padding-bottom: 2px;
}
.list-item[data-target]:hover, .list-item[data-action]:hover {
.list-item[data-target]:hover, .list-item[data-action]:hover,
#about_window .list-item:is(:hover, :focus-within) {
background-color: var(--bg-hover);
}
@@ -565,39 +588,33 @@ select.hidden + .custom-select-wrapper {
}
.custom-select-icon svg { width: 16px; height: 16px; }
.custom-select-options {
position: absolute;
top: calc(100% + 8px);
left: 0;
min-width: 100%;
position: fixed;
display: none;
width: max-content;
white-space: nowrap;
background-color: var(--bg-panel);
border: 1px solid var(--border-color);
border-radius: 10px;
box-shadow: var(--shadow-sm);
z-index: 100;
z-index: 1000;
list-style: none;
margin: 0;
max-height: 0;
padding: 0 6px;
border-width: 0;
overflow: hidden;
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s 0.2s, max-height 0s 0.2s, padding 0s 0.2s, border-width 0s 0.2s, box-shadow 0s 0.2s;
}
.custom-select-wrapper.open .custom-select-options {
max-height: 260px;
padding: 6px;
border-width: 1px;
overflow-y: auto;
opacity: 1;
visibility: visible;
transform: translateY(0);
transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s 0s, max-height 0s 0s, padding 0s 0s, border-width 0s 0s, box-shadow 0s 0s;
overflow-x: hidden;
overscroll-behavior: contain;
overflow-wrap: anywhere;
}
.custom-select-options.open {
display: block;
}
@media (prefers-reduced-motion: no-preference) {
.custom-select-options.open {
animation: select-options-enter 160ms ease-out;
}
@keyframes select-options-enter {
from { opacity: 0; transform: translateY(var(--select-enter-offset)); }
to { opacity: 1; transform: translateY(0); }
}
}
.custom-select-option {
padding: 8px 12px;
@@ -626,13 +643,18 @@ select.hidden + .custom-select-wrapper {
}
.quick-actions {
flex: 1;
flex: 0 0 calc(50% + 40px);
min-width: 0;
display: flex;
flex-direction: column;
gap: 12px;
height: 100%;
justify-content: center;
}
}
.quick-actions .list-item {
gap: 12px;
}
#loading_overlay {
position: fixed;
@@ -648,9 +670,38 @@ select.hidden + .custom-select-wrapper {
transition: opacity 0.4s ease, visibility 0.4s ease;
}
#loading_overlay.fade-out {
opacity: 0;
visibility: hidden;
#loading_overlay.fade-out {
opacity: 0;
visibility: hidden;
}
#loading_overlay.startup-error {
padding: 28px;
background-color: #f5f5f7;
color: #1d1d1f;
}
.startup-error-box {
max-width: 520px;
padding: 24px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 8px;
background: #ffffff;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.12);
}
.startup-error-box h1 {
margin: 0 0 10px;
font-size: 18px;
font-weight: 700;
}
.startup-error-box p {
margin: 0;
font-size: 13px;
line-height: 1.6;
color: #515154;
word-break: break-word;
}
.spinner {
@@ -838,4 +889,54 @@ body.resizing-active * {
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); }
::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); }
@media (prefers-reduced-motion: no-preference) {
:is(.nav-btn, .icon-action-btn, .icon-btn, .ctrl-btn, .search-box) > svg {
transform-origin: center;
transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
:is(.nav-btn, .icon-action-btn):not(:disabled):is(:hover, :focus-visible) > svg {
transform: scale(1.08);
}
.list-item:is(:hover, :focus-within)
> .icon-action-btn:not(:disabled) > svg {
transform: translateX(3px);
}
.icon-btn[data-target]:not(:disabled):is(:hover, :focus-visible) > svg {
transform: translateX(-3px);
}
.ctrl-btn:not(:disabled):is(:hover, :focus-visible) > svg {
transform: scale(1.1);
}
.search-box:focus-within > svg {
transform: rotate(-8deg) scale(1.08);
}
:is(.nav-btn, .ctrl-btn, .icon-action-btn):not(:disabled):active > svg,
.icon-btn[data-target]:not(:disabled):active > svg,
.list-item:active
> .icon-action-btn:not(:disabled) > svg {
transform: scale(0.92);
transition-duration: 100ms;
}
.app-container.fade-in #home_window.active .state-icon > polyline {
animation: pyas-check-draw 300ms ease-in backwards;
}
@keyframes pyas-check-draw {
from {
stroke-dasharray: 12 12;
stroke-dashoffset: 12;
}
to {
stroke-dasharray: 12 12;
stroke-dashoffset: 0;
}
}
}
+34 -23
View File
@@ -120,8 +120,9 @@ window.AppI18n = {
"col_class": "類別",
"col_title": "標題",
"col_repair": "修復項目",
"btn_select_all": "全選",
"ctx_open_loc": "打開檔案所在位置",
"btn_select_all": "全選",
"ctx_open_loc": "打開檔案所在位置",
"ctx_copy_cell": "複製當前欄位文字",
"msg_too_many": "(資料過多,僅顯示前 {0} 筆以確保效能流暢)",
"res_scan_comp": "掃描完成",
"res_virus_det": "發現病毒",
@@ -309,8 +310,9 @@ window.AppI18n = {
"col_class": "类别",
"col_title": "标题",
"col_repair": "修复项目",
"btn_select_all": "全选",
"ctx_open_loc": "打开文件所在位置",
"btn_select_all": "全选",
"ctx_open_loc": "打开文件所在位置",
"ctx_copy_cell": "复制当前字段文本",
"msg_too_many": "(数据过多,仅显示前 {0} 条以确保性能流畅)",
"res_scan_comp": "扫描完成",
"res_virus_det": "发现病毒",
@@ -498,8 +500,9 @@ window.AppI18n = {
"col_class": "Class",
"col_title": "Title",
"col_repair": "Repair Items",
"btn_select_all": "Select All",
"ctx_open_loc": "Open file location",
"btn_select_all": "Select All",
"ctx_open_loc": "Open file location",
"ctx_copy_cell": "Copy current field text",
"msg_too_many": "(Too much data, showing only the top {0} records for performance)",
"res_scan_comp": "Scan Completed",
"res_virus_det": "Virus Detected",
@@ -687,8 +690,9 @@ window.AppI18n = {
"col_class": "クラス",
"col_title": "タイトル",
"col_repair": "修復項目",
"btn_select_all": "すべて選択",
"ctx_open_loc": "ファイルの場所を開く",
"btn_select_all": "すべて選択",
"ctx_open_loc": "ファイルの場所を開く",
"ctx_copy_cell": "現在のフィールドのテキストをコピー",
"msg_too_many": "(データが多すぎるため、パフォーマンスを確保するために上位 {0} 件のみを表示します)",
"res_scan_comp": "スキャン完了",
"res_virus_det": "ウイルスを検出",
@@ -876,8 +880,9 @@ window.AppI18n = {
"col_class": "클래스",
"col_title": "제목",
"col_repair": "복구 항목",
"btn_select_all": "전체 선택",
"ctx_open_loc": "파일 위치 열기",
"btn_select_all": "전체 선택",
"ctx_open_loc": "파일 위치 열기",
"ctx_copy_cell": "현재 필드 텍스트 복사",
"msg_too_many": "(데이터가 너무 많아 성능 유지를 위해 상위 {0}개만 표시합니다)",
"res_scan_comp": "스캔 완료",
"res_virus_det": "바이러스 발견",
@@ -1065,8 +1070,9 @@ window.AppI18n = {
"col_class": "Classe",
"col_title": "Titre",
"col_repair": "Éléments de réparation",
"btn_select_all": "Tout sélectionner",
"ctx_open_loc": "Ouvrir l'emplacement du fichier",
"btn_select_all": "Tout sélectionner",
"ctx_open_loc": "Ouvrir l'emplacement du fichier",
"ctx_copy_cell": "Copier le texte du champ actuel",
"msg_too_many": "(Trop de données, affichage des {0} premiers enregistrements pour des raisons de performances)",
"res_scan_comp": "Analyse terminée",
"res_virus_det": "Virus détecté",
@@ -1254,8 +1260,9 @@ window.AppI18n = {
"col_class": "Clase",
"col_title": "Título",
"col_repair": "Elementos de reparación",
"btn_select_all": "Seleccionar todo",
"ctx_open_loc": "Abrir ubicación de archivo",
"btn_select_all": "Seleccionar todo",
"ctx_open_loc": "Abrir ubicación de archivo",
"ctx_copy_cell": "Copiar texto del campo actual",
"msg_too_many": "(Demasiados datos, mostrando solo los primeros {0} registros para mantener el rendimiento)",
"res_scan_comp": "Escaneo completado",
"res_virus_det": "Virus detectado",
@@ -1443,8 +1450,9 @@ window.AppI18n = {
"col_class": "वर्ग",
"col_title": "शीर्षक",
"col_repair": "मरम्मत आइटम",
"btn_select_all": "सभी चुनें",
"ctx_open_loc": "फ़ाइल स्थान खोलें",
"btn_select_all": "सभी चुनें",
"ctx_open_loc": "फ़ाइल स्थान खोलें",
"ctx_copy_cell": "वर्तमान फ़ील्ड का टेक्स्ट कॉपी करें",
"msg_too_many": "(बहुत अधिक डेटा, प्रदर्शन सुनिश्चित करने के लिए केवल शीर्ष {0} रिकॉर्ड दिखा रहा है)",
"res_scan_comp": "स्कैन पूरा हुआ",
"res_virus_det": "वायरस का पता चला",
@@ -1632,8 +1640,9 @@ window.AppI18n = {
"col_class": "الفئة",
"col_title": "العنوان",
"col_repair": "عناصر الإصلاح",
"btn_select_all": "تحديد الكل",
"ctx_open_loc": "فتح موقع الملف",
"btn_select_all": "تحديد الكل",
"ctx_open_loc": "فتح موقع الملف",
"ctx_copy_cell": "نسخ نص الحقل الحالي",
"msg_too_many": "(بيانات كثيرة جدًا، يتم عرض أول {0} سجل فقط لضمان الأداء)",
"res_scan_comp": "اكتمل الفحص",
"res_virus_det": "تم اكتشاف فيروس",
@@ -1821,8 +1830,9 @@ window.AppI18n = {
"col_class": "Класс",
"col_title": "Заголовок",
"col_repair": "Элементы восстановления",
"btn_select_all": "Выбрать все",
"ctx_open_loc": "Открыть расположение файла",
"btn_select_all": "Выбрать все",
"ctx_open_loc": "Открыть расположение файла",
"ctx_copy_cell": "Копировать текст текущего поля",
"msg_too_many": "(Слишком много данных, показаны только первые {0} записей для обеспечения производительности)",
"res_scan_comp": "Сканирование завершено",
"res_virus_det": "Обнаружен вирус",
@@ -2010,8 +2020,9 @@ window.AppI18n = {
"col_class": "Razred",
"col_title": "Naslov",
"col_repair": "Predmeti za popravilo",
"btn_select_all": "Izberi vse",
"ctx_open_loc": "Odpri mesto datoteke",
"btn_select_all": "Izberi vse",
"ctx_open_loc": "Odpri mesto datoteke",
"ctx_copy_cell": "Kopiraj besedilo trenutnega polja",
"msg_too_many": "(Preveč podatkov, prikazanih je samo prvih {0} zapisov za zagotavljanje zmogljivosti)",
"res_scan_comp": "Skeniranje končano",
"res_virus_det": "Zaznan virus",
@@ -2093,4 +2104,4 @@ window.AppI18n = {
russian_switch: "ru",
slovenian_switch: "sl"
}
};
};
+1287 -1130
View File
File diff suppressed because it is too large. Load diff
+61 -19
View File
@@ -6,7 +6,18 @@
<title>PYAS Security</title>
<link rel="stylesheet" href="/static/css/style.css">
<script defer src="/static/js/i18n.js"></script>
<script defer src="/static/js/main.js"></script>
<script defer src="/static/js/main.js"></script>
<script>
window.addEventListener('DOMContentLoaded', function () {
window.setTimeout(function () {
var overlay = document.getElementById('loading_overlay');
if (overlay && !overlay.classList.contains('fade-out') && !overlay.classList.contains('startup-error')) {
overlay.classList.add('startup-error');
overlay.innerHTML = '<div class="startup-error-box"><h1>PYAS failed to open</h1><p>WebView2 initialization timed out. PYAS will retry automatically.</p></div>';
}
}, 20000);
});
</script>
</head>
<body>
<div id="loading_overlay">
@@ -56,18 +67,20 @@
<div class="state-widget">
<svg viewBox="0 0 24 24" class="state-icon theme-svg" style="width: 120px; height: 120px;" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path><polyline points="9 12 11 14 15 10"></polyline></svg>
<h2 class="state-title" data-i18n>此裝置已受到防護</h2>
</div>
<div class="quick-actions">
<div class="list-item" style="cursor: pointer;" id="quick_scan_btn" data-action="scan">
<div class="item-info">
<h2 data-i18n>智能掃描</h2><p data-i18n>此選項可以快速掃描裝置安全性</p>
</div>
</div>
<div class="quick-actions">
<div class="list-item" style="cursor: pointer;" id="quick_scan_btn" data-action="scan">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="10" cy="10" r="6"/><path d="m14.5 14.5 5 5M8 10l1.5 1.5L12 8.5"/></svg>
<div class="item-info">
<h2 data-i18n>智能掃描</h2><p data-i18n>此選項可以快速掃描裝置安全性</p>
</div>
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
</div>
<div class="list-item" style="cursor: pointer;" data-target="log_export_window">
<div class="item-info">
<h2 data-i18n>日誌導出</h2><p data-i18n>此選項可以篩選日誌並匯出存檔</p>
</div>
</div>
<div class="list-item" style="cursor: pointer;" data-target="log_export_window">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9zM14 3v6h6"/><path d="M8 13h4m-4 4h3m5-5v6m-3-3 3 3 3-3"/></svg>
<div class="item-info">
<h2 data-i18n>日誌導出</h2><p data-i18n>此選項可以篩選日誌並匯出存檔</p>
</div>
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
</div>
</div>
@@ -104,48 +117,56 @@
<section id="protect_window" class="page">
<div class="list-widget">
<div class="list-item">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M8 12h2l1-3 2 6 1-3h2"/></svg>
<div class="item-info">
<h2 data-i18n>進程防護</h2><p data-i18n>此選項可以終止惡意進程運行</p>
</div>
<label class="toggle-switch"><input type="checkbox" checked><span class="slider"></span></label>
</div>
<div class="list-item">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M9 8v8m6-8v8"/></svg>
<div class="item-info">
<h2 data-i18n>進程暫停</h2><p data-i18n>此選項可以暫停進程等待掃描</p>
</div>
<label class="toggle-switch"><input type="checkbox" checked><span class="slider"></span></label>
</div>
<div class="list-item">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m12 3 9 5-9 5-9-5 9-5zM3 12l9 5 9-5M3 16l9 5 9-5"/></svg>
<div class="item-info">
<h2 data-i18n>加載防護</h2><p data-i18n>此選項可以攔截惡意進程加載</p>
</div>
<label class="toggle-switch"><input type="checkbox" checked><span class="slider"></span></label>
</div>
<div class="list-item">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM14 2v6h6m-12 7 3 3 5-5"/></svg>
<div class="item-info">
<h2 data-i18n>檔案防護</h2><p data-i18n>此選項可以隔離惡意檔案建立</p>
</div>
<label class="toggle-switch"><input type="checkbox" checked><span class="slider"></span></label>
</div>
<div class="list-item">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="3" y="3" width="18" height="13" rx="2"/><path d="M8 21h8m-4-5v5M8 10l3 3 5-6"/></svg>
<div class="item-info">
<h2 data-i18n>系統防護</h2><p data-i18n>此選項可以實時修復系統異常</p>
</div>
<label class="toggle-switch"><input type="checkbox" checked><span class="slider"></span></label>
</div>
<div class="list-item">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10zM8 10a6 6 0 0 1 8 0m-6 3a3 3 0 0 1 4 0"/><circle cx="12" cy="16" r=".5"/></svg>
<div class="item-info">
<h2 data-i18n>網路防護</h2><p data-i18n>此選項可以攔截惡意網路連接</p>
</div>
<label class="toggle-switch"><input type="checkbox" checked><span class="slider"></span></label>
</div>
<div class="list-item">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="6" y="6" width="12" height="12" rx="2"/><path d="M9 2v4m6-4v4M9 18v4m6-4v4M2 9h4m-4 6h4m12-6h4m-4 6h4"/><polyline points="9 12 11 14 15 10"/></svg>
<div class="item-info">
<h2 data-i18n>驅動防護</h2><p data-i18n>此選項可以實時攔截惡意行為</p>
</div>
<label class="toggle-switch"><input type="checkbox" checked><span class="slider"></span></label>
</div>
<div class="list-item" style="cursor: pointer;" data-target="custom_protect_window">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4 6h3m4 0h9M4 12h9m4 0h3M4 18h3m4 0h9"/><circle cx="9" cy="6" r="2"/><circle cx="15" cy="12" r="2"/><circle cx="9" cy="18" r="2"/></svg>
<div class="item-info">
<h2 data-i18n>自訂防護</h2><p data-i18n>此選項可以導入自訂防護規則</p>
</div>
@@ -157,48 +178,56 @@
<section id="tool_window" class="page">
<div class="list-widget">
<div class="list-item" style="cursor: pointer;" data-target="taskmgr_window">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="3" y="3" width="18" height="14" rx="2"/><path d="M8 21h8m-4-4v4M6 11h3l2-4 3 7 2-3h2"/></svg>
<div class="item-info">
<h2 data-i18n>進程管理</h2><p data-i18n>此選項可以實時查看系統進程</p>
</div>
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
</div>
<div class="list-item" style="cursor: pointer;" data-target="startup_window">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 3v9m-6.4-6.4a9 9 0 1 0 12.8 0"/></svg>
<div class="item-info">
<h2 data-i18n>啟動項管理</h2><p data-i18n>此選項可以管理各應用啟動項</p>
</div>
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
</div>
<div class="list-item" style="cursor: pointer;" data-target="junk_window">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7m4-7v7"/></svg>
<div class="item-info">
<h2 data-i18n>垃圾清理</h2><p data-i18n>此選項可以清理暫存垃圾檔案</p>
</div>
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
</div>
<div class="list-item" style="cursor: pointer;" data-target="repair_window">
</div>
<div class="list-item" style="cursor: pointer;" data-target="repair_window">
<svg class="item-icon item-icon-repair" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M14 6a5 5 0 0 0-6 6l-5 5a2.1 2.1 0 0 0 3 3l5-5a5 5 0 0 0 6-6l-3 3-3-3 3-3z"/></svg>
<div class="item-info">
<h2 data-i18n>系統修復</h2><p data-i18n>此選項可以修復系統登錄檔項目</p>
</div>
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
</div>
<div class="list-item" style="cursor: pointer;" data-target="popup_window">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 8h18m-12 3 6 6m0-6-6 6"/></svg>
<div class="item-info">
<h2 data-i18n>彈窗攔截</h2><p data-i18n>此選項可以選擇指定窗口攔截</p>
</div>
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
</div>
<div class="list-item" style="cursor: pointer;" data-target="netmon_window">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M5 3v18h16M8 15l4-5 4 3 5-7"/></svg>
<div class="item-info">
<h2 data-i18n>流量監控</h2><p data-i18n>此選項可以實時監控網路流量</p>
</div>
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
</div>
<div class="list-item" style="cursor: pointer;" data-target="whitelist_window">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="4" y="3" width="16" height="18" rx="2"/><path d="m7 8 1 1 2-2m3 1h4m-10 7 2 2 3-4m2 2h3"/></svg>
<div class="item-info">
<h2 data-i18n>白名單</h2><p data-i18n>此選項可以查看並管理白名單</p>
</div>
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
</div>
<div class="list-item" style="cursor: pointer;" data-target="quarantine_window">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 9h18m-13 5h8m-4-4v8M8 5V3h8v2"/></svg>
<div class="item-info">
<h2 data-i18n>隔離區</h2><p data-i18n>此選項可以查看並管理隔離區</p>
</div>
@@ -210,42 +239,49 @@
<section id="setting_window" class="page">
<div class="list-widget">
<div class="list-item">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m13 2-9 12h7l-1 8 10-12h-7l1-8z"/></svg>
<div class="item-info">
<h2 data-i18n>增強模式</h2><p data-i18n>此選項可以增強靜態掃描引擎</p>
</div>
<label class="toggle-switch"><input type="checkbox"><span class="slider"></span></label>
</div>
<div class="list-item">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="4" y="4" width="16" height="16" rx="3"/><path d="M8 12h8m-4-4v8"/></svg>
<div class="item-info">
<h2 data-i18n>擴展引擎</h2><p data-i18n>此選項可以使用擴展規則引擎</p>
</div>
<label class="toggle-switch"><input type="checkbox"><span class="slider"></span></label>
</div>
<div class="list-item">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 18H6a4 4 0 0 1-.8-7.9A7 7 0 0 1 18.7 8a5 5 0 0 1-.7 10h-1m-5 3V11m-4 4 4-4 4 4"/></svg>
<div class="item-info">
<h2 data-i18n>雲端服務</h2><p data-i18n>此選項可以自動上報可疑檔案</p>
</div>
<label class="toggle-switch"><input type="checkbox" checked><span class="slider"></span></label>
</div>
<div class="list-item">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 4h18l-7 8v7l-4 2v-9L3 4z"/></svg>
<div class="item-info">
<h2 data-i18n>副檔名過濾</h2><p data-i18n>此選項僅掃描指定檔案副檔名</p>
</div>
<label class="toggle-switch"><input type="checkbox" checked><span class="slider"></span></label>
</div>
<div class="list-item">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="3" y="3" width="18" height="14" rx="2"/><path d="M8 21h8m-4-4v4M12 6v4m-2.5-2.5a3.5 3.5 0 1 0 5 0"/></svg>
<div class="item-info">
<h2 data-i18n>開機啟動</h2><p data-i18n>此選項可以設定開機自動執行</p>
</div>
<label class="toggle-switch"><input type="checkbox" checked><span class="slider"></span></label>
</div>
<div class="list-item">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 7h10M7 11h3"/><circle cx="13" cy="14" r="3"/><path d="m15.2 16.2 2.8 2.8"/></svg>
<div class="item-info">
<h2 data-i18n>選單掃描</h2><p data-i18n>此選項可以啟用右鍵掃描功能</p>
</div>
<label class="toggle-switch"><input type="checkbox"><span class="slider"></span></label>
</div>
<div class="list-item">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 3a9 9 0 1 0 0 18h1a2 2 0 0 0 1.4-3.4 1.5 1.5 0 0 1 1.1-2.6H18a3 3 0 0 0 3-3 9 9 0 0 0-9-9z"/><circle cx="7.5" cy="10" r=".7"/><circle cx="11" cy="7" r=".7"/><circle cx="15.5" cy="8" r=".7"/></svg>
<div class="item-info">
<h2 data-i18n>顯色主題</h2><p data-i18n>此選項可以選擇主題顏色</p>
</div>
@@ -260,6 +296,7 @@
</select>
</div>
<div class="list-item">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 5h12M9 2v3m3 0c-1 6-4 9-9 12M5 8c1.5 4 4 6 8 8m0 5 4-10 4 10m-6.5-4h5"/></svg>
<div class="item-info">
<h2 data-i18n>顯示語言</h2><p data-i18n>此選項可以選擇顯示語言</p>
</div>
@@ -278,6 +315,7 @@
</select>
</div>
<div class="list-item">
<svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 10a9 9 0 1 1 2 8M3 4v6h6m3-3v5l3 2"/></svg>
<div class="item-info">
<h2 data-i18n>重置選項</h2><p data-i18n>此選項可以重置所有設定</p>
</div>
@@ -289,27 +327,30 @@
<section id="about_window" class="page">
<div class="list-widget">
<div class="list-item">
<svg class="item-icon item-icon-compact" aria-hidden="true" focusable="false" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"></path></svg>
<div class="item-info">
<h2 data-i18n>產品金鑰</h2><p data-i18n>此選項可以啟用產品金鑰</p>
</div>
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"></path></svg></button>
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
</div>
<div class="list-item">
<svg class="item-icon item-icon-compact" aria-hidden="true" focusable="false" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
<div class="item-info">
<h2 data-i18n>官方網站</h2><p data-i18n>此選項可以前往官方網站</p>
</div>
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg></button>
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
</div>
<div class="list-item">
<svg class="item-icon" aria-hidden="true" focusable="false" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10a9 9 0 1 0-2 8M21 4v6h-6"/></svg>
<div class="item-info">
<h2 data-i18n>檢查更新</h2><p data-i18n>此選項可以檢查可用更新</p>
</div>
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg></button>
<button class="icon-action-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
</div>
<div class="license-widget">
<textarea class="license-text" readonly>87owo Publicly Available Source Code License Agreement
Last Updated: 2026-06-09
Last Updated: 2026-07-18
IMPORTANT NOTICE: THIS LICENSE IS "SOURCE AVAILABLE" AND IS NOT AN OPEN SOURCE LICENSE APPROVED BY THE OPEN SOURCE INITIATIVE (OSI). BY DOWNLOADING, ACCESSING, OR USING THE SOFTWARE, YOU AGREE TO BE BOUND BY THESE TERMS.
1. Definitions and Interpretation
@@ -361,6 +402,7 @@ IMPORTANT NOTICE: THIS LICENSE IS "SOURCE AVAILABLE" AND IS NOT AN OPEN SOURCE L
- 7.3. Scope of Collected Extensions: Sample collection targets potential executable and system binary formats, which explicitly include, but are not limited to, files with the following extensions: `.exe`, `.dll`, `.sys`, `.ocx`, `.scr`, `.efi`, `.acm`, `.ax`, `.cpl`, `.drv`, `.com`, `.mui`, `.pyd`, `.wfx`, `.api`, `.awx`, `.rll`, and `.winmd`.
- 7.4. Public Disclosure of Threat Intelligence: You explicitly acknowledge and agree that the metadata, behavioral reports, and threat intelligence data generated from the cloud analysis of submitted samples may be publicly disclosed, published, or shared with the cybersecurity community for collaborative defense purposes.
- 7.5. Opt-Out Mechanism: Cloud-based telemetry and sample submission are fully optional. Users who do not wish to participate in sample collection or cloud-assisted analysis may disable the corresponding cloud services at any time via the toggle switches provided within the Software’s settings interface.
- 7.6. Driver Signature: Certain kernel-mode drivers distributed with this software may be digitally signed using code signing methods that are not certified by Microsoft Windows Hardware Quality Labs (WHQL). By installing or using this software, you acknowledge this and decide for yourself whether to install and trust such drivers on your system.
8. Warranty Disclaimer and Limitation of Liability
@@ -579,4 +621,4 @@ IMPORTANT NOTICE: THIS LICENSE IS "SOURCE AVAILABLE" AND IS NOT AN OPEN SOURCE L
</div>
</div>
</body>
</html>
</html>
+2 -1
View File
@@ -1,6 +1,6 @@
87owo Publicly Available Source Code License Agreement
Last Updated: 2026-06-09
Last Updated: 2026-07-18
IMPORTANT NOTICE: THIS LICENSE IS "SOURCE AVAILABLE" AND IS NOT AN OPEN SOURCE LICENSE APPROVED BY THE OPEN SOURCE INITIATIVE (OSI). BY DOWNLOADING, ACCESSING, OR USING THE SOFTWARE, YOU AGREE TO BE BOUND BY THESE TERMS.
1. Definitions and Interpretation
@@ -52,6 +52,7 @@ IMPORTANT NOTICE: THIS LICENSE IS "SOURCE AVAILABLE" AND IS NOT AN OPEN SOURCE L
- 7.3. Scope of Collected Extensions: Sample collection targets potential executable and system binary formats, which explicitly include, but are not limited to, files with the following extensions: `.exe`, `.dll`, `.sys`, `.ocx`, `.scr`, `.efi`, `.acm`, `.ax`, `.cpl`, `.drv`, `.com`, `.mui`, `.pyd`, `.wfx`, `.api`, `.awx`, `.rll`, and `.winmd`.
- 7.4. Public Disclosure of Threat Intelligence: You explicitly acknowledge and agree that the metadata, behavioral reports, and threat intelligence data generated from the cloud analysis of submitted samples may be publicly disclosed, published, or shared with the cybersecurity community for collaborative defense purposes.
- 7.5. Opt-Out Mechanism: Cloud-based telemetry and sample submission are fully optional. Users who do not wish to participate in sample collection or cloud-assisted analysis may disable the corresponding cloud services at any time via the toggle switches provided within the Software’s settings interface.
- 7.6. Driver Signature: Certain kernel-mode drivers distributed with this software may be digitally signed using code signing methods that are not certified by Microsoft Windows Hardware Quality Labs (WHQL). By installing or using this software, you acknowledge this and decide for yourself whether to install and trust such drivers on your system.
8. Warranty Disclaimer and Limitation of Liability
+1471 -1178
View File
File diff suppressed because it is too large. Load diff
+248
View File
@@ -0,0 +1,248 @@
import os, time, uuid, logging, requests, hashlib
from pathlib import Path
from typing import Optional, List, Dict, Any, Union
from concurrent.futures import ThreadPoolExecutor
####################################################################################################
class PYAS_Client:
def __init__(self, api_key: str, hosts: Union[str, List[str]], timeout: int = 30):
self.timeout = timeout
self.logger = logging.getLogger("PYAS_API")
self.session = requests.Session()
self.session.headers.update({"X-API-Key": api_key, "User-Agent": "PYAS-API/1.1"})
if isinstance(hosts, str):
self.base_url = hosts.rstrip('/')
else:
self.base_url = self._get_fastest_host(hosts)
self.logger.info(f"Initialized PYAS API Client targeting {self.base_url}")
def _get_fastest_host(self, hosts: List[str]) -> str:
def ping(host: str):
url = host.rstrip('/')
try:
start = time.perf_counter()
r = self.session.head(url, timeout=3)
if r.status_code < 500:
return url, time.perf_counter() - start
except Exception:
pass
return url, float('inf')
self.logger.info("Testing host latencies...")
with ThreadPoolExecutor(max_workers=min(len(hosts), 10)) as executor:
results = list(executor.map(ping, hosts))
valid_results = []
for url, latency in results:
if latency != float('inf'):
self.logger.info(f"Host: {url} -> {latency:.4f}s")
valid_results.append((url, latency))
else:
self.logger.warning(f"Host: {url} -> Timeout/Error")
if not valid_results:
self.logger.warning("All host latency tests failed. Falling back to the first host.")
return hosts[0].rstrip('/')
best_host, best_time = min(valid_results, key=lambda x: x[1])
self.logger.info(f"Selected fastest host: {best_host} ({best_time:.4f}s)")
return best_host
def _request(self, method: str, endpoint: str, **kwargs) -> Optional[requests.Response]:
self.logger.debug(f"Request: {method} {endpoint}")
try:
r = self.session.request(method, f"{self.base_url}{endpoint}", timeout=self.timeout, **kwargs)
if r.status_code == 200:
return r
error_map = {401: "API Key invalid", 402: "Insufficient points", 404: "Not found", 413: "Payload too large", 500: "Server Error"}
self.logger.error(f"[{endpoint}] Failed: {error_map.get(r.status_code, f'HTTP {r.status_code}')}")
if 400 <= r.status_code < 500:
setattr(r, 'is_fatal', True)
return r
except Exception as e:
self.logger.error(f"Connection error: {e}")
return None
####################################################################################################
def _calculate_sha256(self, path: Path) -> str:
self.logger.debug(f"Calculating SHA256 for {path}")
sha256_hash = hashlib.sha256()
with open(path, "rb") as f:
for byte_block in iter(lambda: f.read(1048576), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
def upload_file(self, file_path: str, chunk_size: int = 4194304, max_retries: int = 3) -> Optional[str]:
path = Path(file_path)
if not path.exists():
self.logger.error(f"File not found: {file_path}")
return None
file_size = path.stat().st_size
self.logger.info(f"Initiating unified chunked upload for {path.name} ({file_size} bytes)")
if file_size > 268435456:
self.logger.error(f"File size exceeds 256MB limit: {file_size} bytes")
return None
local_sha256 = self._calculate_sha256(path)
status_req = self._request("GET", f"/api/processing_status/{local_sha256}")
if status_req and status_req.status_code == 200:
if status_req.json().get('status') in ['done', 'processing', 'queued']:
self.logger.info(f"File already exists or processing on server: {local_sha256}")
return local_sha256
return self._upload_chunked(path, chunk_size, file_size, max_retries)
####################################################################################################
def _upload_chunked(self, path: Path, chunk_size: int, file_size: int, max_retries: int = 3) -> Optional[str]:
total_chunks = max(1, (file_size + chunk_size - 1) // chunk_size)
upload_id = uuid.uuid4().hex
self.logger.info(f"Starting chunked upload for {path.name} (Chunks: {total_chunks}, ID: {upload_id})")
with open(path, 'rb') as f:
for i in range(total_chunks):
chunk_data = f.read(chunk_size)
headers = {
"X-Chunk-Index": str(i),
"X-Total-Chunks": str(total_chunks),
"X-Upload-ID": upload_id
}
chunk_success = False
for attempt in range(max_retries):
r = self._request("POST", "/api/upload", files={'file': (path.name, chunk_data)}, headers=headers)
if r and r.status_code == 200:
chunk_success = True
self.logger.debug(f"Uploaded chunk {i + 1}/{total_chunks}")
if i == total_chunks - 1:
try:
return r.json().get('url', '').split('/')[-1] or None
except Exception as e:
self.logger.error(f"Failed to parse final chunk response: {e}")
return None
break
if hasattr(r, 'is_fatal'):
self.logger.error(f"Fatal upload error encountered at chunk {i}. Aborting.")
return None
wait_time = 2 ** attempt
self.logger.warning(f"Chunk upload retry {attempt + 1}/{max_retries} for chunk {i}. Waiting {wait_time}s")
time.sleep(wait_time)
if not chunk_success:
self.logger.error(f"Failed to upload chunk {i + 1}/{total_chunks} after {max_retries} attempts")
return None
return None
####################################################################################################
def wait_for_analysis(self, sha256: str, interval: int = 5, max_retries: int = 60) -> bool:
self.logger.info(f"Waiting for analysis completion: {sha256}")
for attempt in range(max_retries):
r = self._request("GET", f"/api/processing_status/{sha256}")
if r and r.status_code == 200:
status = r.json().get('status')
self.logger.debug(f"Analysis status: {status}")
if status == 'done':
self.logger.info(f"Analysis completed: {sha256}")
return True
if status in ['missing', 'error', 'failed']:
self.logger.error(f"Analysis failed with status: {status}")
return False
time.sleep(interval)
self.logger.error(f"Analysis polling timed out for {sha256}")
return False
def get_report(self, sha256: str) -> Optional[Dict[str, Any]]:
self.logger.info(f"Fetching report for {sha256}")
r = self._request("GET", f"/api/report/{sha256}")
if r:
return r.json()
self.logger.error(f"Failed to fetch report for {sha256}")
return None
####################################################################################################
def rescan(self, sha256: str) -> bool:
self.logger.info(f"Triggering rescan for {sha256}")
r = self._request("POST", f"/api/rescan/{sha256}")
success = bool(r and r.json().get('status') == 'success')
if success:
self.logger.info(f"Rescan triggered successfully for {sha256}")
else:
self.logger.error(f"Rescan failed for {sha256}")
return success
def search_files(self, query: str, limit: int = 50, page: int = 0, snapshot: Optional[str] = None) -> List[Dict[str, Any]]:
self.logger.info(f"Searching files (Query: '{query}', Limit: {limit}, Page: {page})")
payload = {'query': query, 'page': page}
if snapshot:
payload['snapshot'] = snapshot
r = self._request("POST", "/api/search_more", data=payload)
results = r.json().get('results', [])[:limit] if r else []
self.logger.info(f"Search returned {len(results)} results")
return results
def download_sample(self, sha256: str, save_dir: str = "downloads") -> Optional[str]:
self.logger.info(f"Downloading sample {sha256}")
os.makedirs(save_dir, exist_ok=True)
path = os.path.join(save_dir, f"{sha256}.zip")
r = self._request("GET", f"/api/download/{sha256}", stream=True)
if r:
with open(path, 'wb') as f:
for chunk in r.iter_content(8192):
if chunk:
f.write(chunk)
self.logger.info(f"Sample downloaded successfully to {path}")
return path
self.logger.error(f"Failed to download sample {sha256}")
return None
####################################################################################################
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] %(levelname)s [%(name)s] - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
API_KEY = "" # https://pyas-security.com/setting?tab=application
HOSTS = [
"https://pyas-security.com/",
"https://cn.pyas-security.com/"
]
client = PYAS_Client(API_KEY, HOSTS)
target_file = r"C:/Windows/explorer.exe"
sha = client.upload_file(target_file)
if sha and client.wait_for_analysis(sha):
report = client.get_report(sha)
if report:
meta = report.get('data', {}).get('metadata', {})
logging.info(f"Result: {meta.get('label')} ({meta.get('score')}%)")
for item in client.search_files('date>=2020; ext=exe | dll', limit=3):
client.download_sample(item['sha256'])
+1322 -970
View File
File diff suppressed because it is too large. Load diff
+216 -154
View File
@@ -1,6 +1,6 @@
#define AppId "{{a7d7bac3-93b8-4630-8308-c7a56bf7fdf4}"
#define AppName "PYAS"
#define AppVersion "3.6.2.0"
#define AppVersion "3.7.0.0"
#define AppPublisher "PYAS Security"
#define AppURL "https://github.com/87owo/PYAS"
#define AppExeName "PYAS.exe"
@@ -52,20 +52,22 @@ Name: "arabic"; MessagesFile: "SetupResources\Arabic.isl"
Name: "russian"; MessagesFile: "SetupResources\Russian.isl"
Name: "slovenian"; MessagesFile: "SetupResources\Slovenian.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"
Name: "autostart"; Description: "{cm:AutoStartTask}"; GroupDescription: "{cm:AdditionalIcons}"
Name: "install_webview2"; Description: "{cm:InstallWebView2}"; GroupDescription: "{cm:Dependencies}"
Name: "install_vcredist"; Description: "{cm:InstallVCRedist}"; GroupDescription: "{cm:Dependencies}"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"
[Files]
Source: "Payload\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "Redist\VC_redist.x64.exe"; DestDir: "{tmp}\PYAS_Redist"; Flags: deleteafterinstall ignoreversion
Source: "Redist\MicrosoftEdgeWebview2Setup.exe"; DestDir: "{tmp}\PYAS_Redist"; Flags: deleteafterinstall ignoreversion
Source: "Redist\VC_redist.x64.exe"; DestDir: "{tmp}\PYAS_Redist"; Flags: deleteafterinstall ignoreversion; AfterInstall: EnsureVCRedist
Source: "Redist\MicrosoftEdgeWebview2Setup.exe"; DestDir: "{tmp}\PYAS_Redist"; Flags: deleteafterinstall ignoreversion; AfterInstall: EnsureWebView2
Source: "Payload\Engine\*"; DestDir: "{app}\Engine"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "Payload\Interface\*"; DestDir: "{app}\Interface"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "Payload\License\*"; DestDir: "{app}\License"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "Payload\PYAS.exe"; DestDir: "{app}"; Flags: ignoreversion; AfterInstall: FinalizeDriverRemoval
Source: "Payload\Plugins\*"; DestDir: "{app}\Plugins"; Flags: ignoreversion recursesubdirs createallsubdirs
[Registry]
Root: HKCU; Subkey: "Software\Classes\*\shell\PYAS_Scan"; Flags: uninsdeletekey dontcreatekey
Root: HKCU; Subkey: "Software\Classes\Directory\shell\PYAS_Scan"; Flags: uninsdeletekey dontcreatekey
[Registry]
Root: HKCU; Subkey: "Software\Classes\*\shell\PYAS_Scan"; Flags: uninsdeletekey dontcreatekey
Root: HKCU; Subkey: "Software\Classes\Directory\shell\PYAS_Scan"; Flags: uninsdeletekey dontcreatekey
Root: HKCU; Subkey: "Software\Microsoft\Windows\CurrentVersion\Run"; ValueName: "PYAS_Security"; Flags: uninsdeletevalue dontcreatekey
[UninstallDelete]
Type: filesandordirs; Name: "{commonappdata}\PYAS"
@@ -76,9 +78,7 @@ Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"
Name: "{group}\Uninstall {#AppName}"; Filename: "{uninstallexe}"
[Run]
Filename: "{tmp}\PYAS_Redist\VC_redist.x64.exe"; Parameters: "/quiet /norestart"; Flags: waituntilterminated runhidden; StatusMsg: "{cm:InstallingVCRuntime}"; Tasks: install_vcredist
Filename: "{tmp}\PYAS_Redist\MicrosoftEdgeWebview2Setup.exe"; Parameters: "/silent /install"; Flags: waituntilterminated runhidden; StatusMsg: "{cm:InstallingWebView2Runtime}"; Tasks: install_webview2
Filename: "{app}\{#AppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(AppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent runascurrentuser
Filename: "{app}\{#AppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(AppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent runascurrentuser; Check: CanLaunchApp
[CustomMessages]
english.InstallingVCRuntime=Installing Microsoft Visual C++ Runtime...
@@ -87,9 +87,6 @@ chinesetraditional.InstallingVCRuntime=正在安裝 Microsoft Visual C++ 執行
english.InstallingWebView2Runtime=Installing Microsoft Edge WebView2 Runtime...
chinesesimplified.InstallingWebView2Runtime=正在安装 Microsoft Edge WebView2 Runtime...
chinesetraditional.InstallingWebView2Runtime=正在安裝 Microsoft Edge WebView2 Runtime...
english.AutoStartTask=Run PYAS automatically at system startup
chinesesimplified.AutoStartTask=开机时自动运行 PYAS
chinesetraditional.AutoStartTask=開機時自動執行 PYAS
english.Dependencies=Dependencies:
chinesesimplified.Dependencies=运行环境:
chinesetraditional.Dependencies=執行環境:
@@ -100,154 +97,219 @@ english.InstallVCRedist=Install Microsoft Visual C++ Runtime
chinesesimplified.InstallVCRedist=安装 Microsoft Visual C++ 运行库
chinesetraditional.InstallVCRedist=安裝 Microsoft Visual C++ 執行庫
english.LegacyVersionDetected=An older version of PYAS was detected. Please uninstall it manually before installing.
english.QuitFailed=PYAS or its driver could not be stopped safely. Restart Windows and run Setup again.
english.DependencyInstallFailed=Required Microsoft runtime installation failed. Setup cannot continue.
chinesesimplified.LegacyVersionDetected=检测到存在旧版 PYAS。请先手动卸载旧版后,再运行本安装程序。
chinesetraditional.LegacyVersionDetected=檢測到存在舊版 PYAS。請先手動卸載舊版後,再執行本安裝程式。
[Code]
var
TasksInitialized: Boolean;
function IsWebView2Installed: Boolean;
var
RegKeyPath: string;
Version: string;
begin
RegKeyPath := 'SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}';
Result := False;
if RegQueryStringValue(HKLM, 'SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}', 'pv', Version) then
if Length(Version) > 0 then Result := True;
if not Result and RegQueryStringValue(HKCU, RegKeyPath, 'pv', Version) then
if Length(Version) > 0 then Result := True;
if not Result and RegQueryStringValue(HKLM, RegKeyPath, 'pv', Version) then
if Length(Version) > 0 then Result := True;
end;
function IsVCRedistInstalled: Boolean;
var
Bld: Cardinal;
begin
Result := False;
if RegQueryDWordValue(HKLM, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'Bld', Bld) then
if Bld > 0 then Result := True;
end;
procedure CurPageChanged(CurPageID: Integer);
var
I: Integer;
ItemText: string;
begin
if (CurPageID = wpSelectTasks) and not TasksInitialized then
begin
TasksInitialized := True;
for I := 0 to WizardForm.TasksList.Items.Count - 1 do
begin
ItemText := WizardForm.TasksList.Items[I];
if Pos('Visual C++', ItemText) > 0 then
WizardForm.TasksList.Checked[I] := not IsVCRedistInstalled
else if Pos('WebView2', ItemText) > 0 then
WizardForm.TasksList.Checked[I] := not IsWebView2Installed;
end;
end;
end;
procedure QuitOldInstance(InstallPath: string);
var
ResultCode: Integer;
ExePath: string;
begin
ExePath := InstallPath + '\{#AppExeName}';
if FileExists(ExePath) then
begin
Exec(ExePath, '-quit', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Sleep(500);
Exec(ExpandConstant('{sys}\taskkill.exe'), '/F /IM {#AppExeName} /T', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
end;
end;
function InitializeSetup(): Boolean;
var
OldInstallPath: string;
LegacyPath: string;
begin
LegacyPath := ExpandConstant('{pf32}\{#AppName}');
if DirExists(LegacyPath) and FileExists(LegacyPath + '\{#AppExeName}') then
begin
MsgBox(CustomMessage('LegacyVersionDetected'), mbCriticalError, MB_OK);
Result := False;
Exit;
end;
if RegQueryStringValue(HKLM, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\' + ExpandConstant('{#AppId}') + '_is1', 'InstallLocation', OldInstallPath) or
RegQueryStringValue(HKCU, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\' + ExpandConstant('{#AppId}') + '_is1', 'InstallLocation', OldInstallPath) then
begin
QuitOldInstance(OldInstallPath);
end;
Result := True;
end;
function InitializeUninstall(): Boolean;
begin
QuitOldInstance(ExpandConstant('{app}'));
Result := True;
end;
procedure TryCreateStartupTask;
var
ResultCode: Integer;
PSExe: string;
PSCommand: string;
AppPath: string;
begin
AppPath := ExpandConstant('{app}\{#AppExeName}');
PSExe := ExpandConstant('{sys}\WindowsPowerShell\v1.0\powershell.exe');
PSCommand := '-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -Command ' +
'$Action = New-ScheduledTaskAction -Execute ''' + AppPath + ''' -Argument ''-hide''; ' +
'$Trigger = New-ScheduledTaskTrigger -AtLogOn; ' +
'$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries; ' +
'Register-ScheduledTask -TaskName ''PYAS_Security_ATS'' -Action $Action -Trigger $Trigger -Settings $Settings -RunLevel Highest -Force';
if not Exec(PSExe, PSCommand, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
begin
Log(Format('Failed to create scheduled task, Exec error code: %d', [ResultCode]));
end
else if ResultCode <> 0 then
begin
Log(Format('powershell returned exit code %d while creating the scheduled task', [ResultCode]));
end;
end;
procedure TryDeleteStartupTask;
var
ResultCode: Integer;
begin
if not Exec(ExpandConstant('{sys}\schtasks.exe'), '/Delete /TN "PYAS_Security_ATS" /F', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
begin
Log(Format('Failed to delete scheduled task, Exec error code: %d', [ResultCode]));
end
else if ResultCode <> 0 then
begin
Log(Format('schtasks returned exit code %d while deleting the scheduled task', [ResultCode]));
end;
end;
[Code]
var
DependencyRestartRequired: Boolean;
DependenciesReady: Boolean;
function FindWindow(lpClassName: LongWord; lpWindowName: string): HWND;
external '[email protected] stdcall';
function GetTickCount: Cardinal;
external '[email protected] stdcall';
function IsValidRuntimeVersion(Version: string): Boolean;
begin
Result := (Length(Trim(Version)) > 0) and (Trim(Version) <> '0.0.0.0');
end;
function IsWebView2Installed: Boolean;
var
Version: string;
begin
Result := False;
if RegQueryStringValue(HKLM, 'SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}', 'pv', Version) then
Result := IsValidRuntimeVersion(Version);
if not Result and RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}', 'pv', Version) then
Result := IsValidRuntimeVersion(Version);
if not Result and RegQueryStringValue(HKCU, 'SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}', 'pv', Version) then
Result := IsValidRuntimeVersion(Version);
end;
function IsVCRedistInstalled: Boolean;
var
Installed: Cardinal;
Version: string;
begin
Result := False;
if RegQueryDWordValue(HKLM64, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'Installed', Installed) and (Installed = 1) then
begin
if RegQueryStringValue(HKLM64, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'Version', Version) then
Result := IsValidRuntimeVersion(Version)
else
Result := True;
end;
end;
function DependencyExitCodeSucceeded(ResultCode: Integer): Boolean;
begin
Result := (ResultCode = 0) or (ResultCode = 1641) or (ResultCode = 3010);
if (ResultCode = 1641) or (ResultCode = 3010) then
DependencyRestartRequired := True;
end;
procedure EnsureVCRedist;
var
ResultCode: Integer;
InstallerPath: string;
begin
if IsVCRedistInstalled then Exit;
InstallerPath := ExpandConstant('{tmp}\PYAS_Redist\VC_redist.x64.exe');
WizardForm.StatusLabel.Caption := CustomMessage('InstallingVCRuntime');
if not Exec(InstallerPath, '/quiet /norestart', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) or
not DependencyExitCodeSucceeded(ResultCode) or not IsVCRedistInstalled then
begin
DependenciesReady := False;
RaiseException(CustomMessage('DependencyInstallFailed'));
end;
end;
procedure EnsureWebView2;
var
ResultCode: Integer;
InstallerPath: string;
begin
if IsWebView2Installed then Exit;
InstallerPath := ExpandConstant('{tmp}\PYAS_Redist\MicrosoftEdgeWebview2Setup.exe');
WizardForm.StatusLabel.Caption := CustomMessage('InstallingWebView2Runtime');
if not Exec(InstallerPath, '/silent /install', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) or
not DependencyExitCodeSucceeded(ResultCode) or not IsWebView2Installed then
begin
DependenciesReady := False;
RaiseException(CustomMessage('DependencyInstallFailed'));
end;
end;
function WaitForMainWindowToClose(TimeoutMs: Cardinal): Boolean;
var
Deadline: Cardinal;
begin
Deadline := GetTickCount + TimeoutMs;
repeat
if FindWindow(0, 'PYAS Security') = 0 then
begin
Result := True;
Exit;
end;
Sleep(100);
until GetTickCount >= Deadline;
Result := False;
end;
function QuitOldInstance(InstallPath: string): Boolean;
var
ResultCode: Integer;
ExePath: string;
begin
Result := True;
ExePath := InstallPath + '\{#AppExeName}';
if not FileExists(ExePath) then Exit;
if not Exec(ExePath, '-quit', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
begin
Result := False;
Exit;
end;
Result := (ResultCode = 0) and WaitForMainWindowToClose(30000);
end;
procedure FinalizeDriverRemoval;
var
ResultCode: Integer;
ExePath: string;
begin
ExePath := ExpandConstant('{app}\{#AppExeName}');
if not Exec(ExePath, '-quit', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) or (ResultCode <> 0) then
RaiseException(CustomMessage('QuitFailed'));
end;
function InitializeSetup(): Boolean;
var
OldInstallPath: string;
LegacyPath: string;
begin
DependencyRestartRequired := False;
DependenciesReady := True;
LegacyPath := ExpandConstant('{pf32}\{#AppName}');
if DirExists(LegacyPath) and FileExists(LegacyPath + '\{#AppExeName}') then
begin
MsgBox(CustomMessage('LegacyVersionDetected'), mbCriticalError, MB_OK);
Result := False;
Exit;
end;
if RegQueryStringValue(HKLM, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\' + ExpandConstant('{#AppId}') + '_is1', 'InstallLocation', OldInstallPath) or
RegQueryStringValue(HKCU, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\' + ExpandConstant('{#AppId}') + '_is1', 'InstallLocation', OldInstallPath) then
begin
if not QuitOldInstance(OldInstallPath) then
begin
MsgBox(CustomMessage('QuitFailed'), mbCriticalError, MB_OK);
Result := False;
Exit;
end;
end;
Result := True;
end;
function InitializeUninstall(): Boolean;
begin
Result := QuitOldInstance(ExpandConstant('{app}'));
if not Result then
MsgBox(CustomMessage('QuitFailed'), mbCriticalError, MB_OK);
end;
function CanLaunchApp: Boolean;
begin
Result := DependenciesReady and not DependencyRestartRequired;
end;
function NeedRestart: Boolean;
begin
Result := DependencyRestartRequired;
end;
procedure RemoveLegacyStartupTask;
var
TaskService: Variant;
RootFolder: Variant;
begin
try
TaskService := CreateOleObject('Schedule.Service');
TaskService.Connect();
RootFolder := TaskService.GetFolder('\');
try
RootFolder.DeleteTask('PYAS_Security_ATS', 0);
Log('Removed legacy PYAS scheduled startup task');
except
Log('Legacy PYAS scheduled startup task was not present or could not be removed');
end;
except
Log('Task Scheduler COM was unavailable while cleaning the legacy startup task');
end;
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssPostInstall then
begin
if WizardIsTaskSelected('autostart') then
begin
TryCreateStartupTask;
end;
end;
if CurStep = ssPostInstall then
begin
RemoveLegacyStartupTask;
end;
end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
begin
if CurUninstallStep = usUninstall then
begin
TryDeleteStartupTask;
if CurUninstallStep = usUninstall then
begin
RemoveLegacyStartupTask;
end
else if CurUninstallStep = usPostUninstall then
begin
DelTree(ExpandConstant('{app}'), True, True, True);
end;
end;
end;
+482 -127
View File
@@ -3,12 +3,49 @@ import ctypes, ctypes.wintypes
from PYAS_Tools import FILE_NOTIFY_INFORMATION, LUID, MEMORY_BASIC_INFORMATION, POINT, PYAS_FULL_MESSAGE, PYAS_USER_MESSAGE, RECT, SERVICE_STATUS_PROCESS, TOKEN_PRIVILEGES
####################################################################################################
FLT_PORT_FLAG_SYNC_HANDLE = 0x00000001
HRESULT_IO_PENDING = 0x800703E5
ERROR_OPERATION_ABORTED = 995
ERROR_NOT_FOUND = 1168
ERROR_SHARING_VIOLATION = 32
ERROR_LOCK_VIOLATION = 33
WAIT_OBJECT_0 = 0
WAIT_TIMEOUT = 258
INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
GENERIC_READ = 0x80000000
FILE_SHARE_READ = 0x00000001
FILE_SHARE_WRITE = 0x00000002
OPEN_EXISTING = 3
FILE_ATTRIBUTE_NORMAL = 0x00000080
IOCTL_STORAGE_QUERY_PROPERTY = 0x002D1400
IOCTL_STORAGE_GET_HOTPLUG_INFO = 0x002D0C14
STORAGE_DEVICE_PROPERTY = 0
PROPERTY_STANDARD_QUERY = 0
INTERNAL_STORAGE_BUS_TYPES = frozenset({1, 3, 8, 10, 11, 17, 18, 19})
FILE_SCAN_DEBOUNCE_SECONDS = 0.35
FILE_SCAN_RETRY_SECONDS = 0.25
FILE_LOCK_RETRY_SECONDS = 0.5
PYAS_CONNECTION_MAGIC = 0x53415950
PYAS_CONNECTION_VERSION = 1
PROCESS_TERMINATE = 0x0001
PROCESS_VM_READ = 0x0010
PROCESS_QUERY_INFORMATION = 0x0400
PROCESS_SUSPEND_RESUME = 0x0800
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
PYAS_PROCESS_SCAN_ACCESS = PROCESS_TERMINATE | PROCESS_VM_READ | PROCESS_QUERY_INFORMATION | PROCESS_SUSPEND_RESUME | PROCESS_QUERY_LIMITED_INFORMATION
PYAS_PROCESS_NETWORK_ACCESS = PROCESS_TERMINATE | PROCESS_QUERY_INFORMATION | PROCESS_QUERY_LIMITED_INFORMATION
####################################################################################################
class PYAS_CONNECTION_CONTEXT(ctypes.Structure):
_fields_ = [
("Size", ctypes.wintypes.ULONG),
("Version", ctypes.wintypes.ULONG),
("Magic", ctypes.wintypes.ULONG),
("ProcessId", ctypes.wintypes.ULONG)
]
class OVERLAPPED(ctypes.Structure):
_fields_ = [
@@ -19,6 +56,44 @@ class OVERLAPPED(ctypes.Structure):
("hEvent", ctypes.wintypes.HANDLE)
]
class STORAGE_PROPERTY_QUERY(ctypes.Structure):
_fields_ = [
("PropertyId", ctypes.c_int),
("QueryType", ctypes.c_int),
("AdditionalParameters", ctypes.c_ubyte * 1)
]
class STORAGE_DESCRIPTOR_HEADER(ctypes.Structure):
_fields_ = [
("Version", ctypes.wintypes.DWORD),
("Size", ctypes.wintypes.DWORD)
]
class STORAGE_DEVICE_DESCRIPTOR(ctypes.Structure):
_fields_ = [
("Version", ctypes.wintypes.DWORD),
("Size", ctypes.wintypes.DWORD),
("DeviceType", ctypes.c_ubyte),
("DeviceTypeModifier", ctypes.c_ubyte),
("RemovableMedia", ctypes.c_ubyte),
("CommandQueueing", ctypes.c_ubyte),
("VendorIdOffset", ctypes.wintypes.DWORD),
("ProductIdOffset", ctypes.wintypes.DWORD),
("ProductRevisionOffset", ctypes.wintypes.DWORD),
("SerialNumberOffset", ctypes.wintypes.DWORD),
("BusType", ctypes.c_int),
("RawPropertiesLength", ctypes.wintypes.DWORD)
]
class STORAGE_HOTPLUG_INFO(ctypes.Structure):
_fields_ = [
("Size", ctypes.wintypes.DWORD),
("MediaRemovable", ctypes.c_ubyte),
("MediaHotplug", ctypes.c_ubyte),
("DeviceHotplug", ctypes.c_ubyte),
("WriteCacheEnableOverride", ctypes.c_ubyte)
]
####################################################################################################
class ProtectMixin:
@@ -55,97 +130,143 @@ class ProtectMixin:
def init_whitelist(self):
self.manage_named_list("white_list", [self.file_pyas], action="add")
def sync_driver_whitelist(self, file_path, is_add=True):
def _driver_whitelist_patterns(self, file_path, is_directory=None):
normalized_path = self.norm_path(file_path, must_exist=False)
if not normalized_path:
return []
if is_directory is None:
is_directory = os.path.isdir(normalized_path)
drive_letter = os.path.splitdrive(normalized_path)[0]
driver_path = "*" + normalized_path[len(drive_letter):] if drive_letter else normalized_path
patterns = [driver_path]
if is_directory:
descendant_pattern = driver_path.rstrip("\\/") + "\\*"
if descendant_pattern not in patterns:
patterns.append(descendant_pattern)
return patterns
def sync_driver_whitelist(self, file_path, is_add=True, is_directory=None):
with self.lock_driver:
if not self.driver_port:
return False
driver_path = str(file_path)
drive_letter = os.path.splitdrive(driver_path)[0]
if drive_letter:
driver_path = "*" + driver_path[len(drive_letter):]
msg = PYAS_USER_MESSAGE()
msg.Command = 1 if is_add else 2
msg.Path = driver_path
bytes_returned = ctypes.wintypes.DWORD(0)
try:
return self.fltlib.FilterSendMessage(self.driver_port, ctypes.byref(msg), ctypes.sizeof(msg), None, 0, ctypes.byref(bytes_returned)) == 0
except Exception:
patterns = self._driver_whitelist_patterns(file_path, is_directory)
if not patterns:
return False
success = True
for driver_path in patterns:
msg = PYAS_USER_MESSAGE()
msg.Command = 1 if is_add else 2
bytes_returned = ctypes.wintypes.DWORD(0)
try:
msg.Path = driver_path
if self.fltlib.FilterSendMessage(self.driver_port, ctypes.byref(msg), ctypes.sizeof(msg), None, 0, ctypes.byref(bytes_returned)) != 0:
success = False
except Exception:
success = False
return success
####################################################################################################
def lock_file(self, target_path, lock, quiet=False):
def _try_lock_file(self, file_path):
fd = None
try:
fd = os.open(file_path, os.O_RDWR | os.O_BINARY)
try:
size = os.path.getsize(file_path)
except Exception:
size = 1
lock_size = size if size > 0 else 1
msvcrt.locking(fd, msvcrt.LK_NBRLCK, lock_size)
self.virus_lock[file_path] = (fd, lock_size)
return True, None
except Exception as e:
if fd is not None:
try:
os.close(fd)
except Exception:
pass
return False, e
def _queue_file_lock(self, file_path):
self._schedule_file_task("lock", file_path, self._retry_file_lock, FILE_LOCK_RETRY_SECONDS)
def _retry_file_lock(self, file_path):
if not os.path.isfile(file_path):
return
with self.lock_file_ops:
if lock:
if not os.path.exists(target_path):
return
if file_path in self.virus_lock:
return
success, _ = self._try_lock_file(file_path)
paths_to_lock = []
if os.path.isdir(target_path):
for root, _, files in os.walk(target_path):
for f in files:
paths_to_lock.append(os.path.join(root, f))
else:
paths_to_lock.append(target_path)
if not success:
self._queue_file_lock(file_path)
def lock_file(self, target_path, lock, quiet=False):
if lock:
if not os.path.exists(target_path):
return False
paths_to_lock = []
if os.path.isdir(target_path):
for root, _, files in os.walk(target_path):
for file_name in files:
paths_to_lock.append(os.path.join(root, file_name))
else:
paths_to_lock.append(target_path)
failed_locks = []
with self.lock_file_ops:
for file_path in paths_to_lock:
if file_path in self.virus_lock:
continue
success = False
last_error = None
for _ in range(5):
try:
fd = os.open(file_path, os.O_RDWR | os.O_BINARY)
try:
try:
size = os.path.getsize(file_path)
except Exception:
size = 1
success, error = self._try_lock_file(file_path)
if not success:
failed_locks.append((file_path, error))
lock_size = size if size > 0 else 1
msvcrt.locking(fd, msvcrt.LK_NBRLCK, lock_size)
self.virus_lock[file_path] = (fd, lock_size)
success = True
break
for file_path, error in failed_locks:
self._queue_file_lock(file_path)
if not quiet:
self.write_log("INFO", "File Lock Deferred", source=file_path, detail=str(error))
except Exception as e:
os.close(fd)
raise e
return not failed_locks
except Exception as e:
last_error = e
time.sleep(0.1)
self._cancel_pending_file_tasks("lock", target_path)
with self.lock_file_ops:
target_norm = os.path.normcase(target_path)
target_dir = target_norm + os.sep
keys_to_unlock = []
if not success and not quiet:
self.write_log("WARN", "lock_file", source=file_path, detail=str(last_error), success=False)
else:
target_norm = os.path.normcase(target_path)
target_dir = target_norm + os.sep
keys_to_unlock = []
for locked_path in self.virus_lock:
locked_norm = os.path.normcase(locked_path)
if locked_norm == target_norm or locked_norm.startswith(target_dir):
keys_to_unlock.append(locked_path)
for locked_path in self.virus_lock:
locked_norm = os.path.normcase(locked_path)
if locked_norm == target_norm or locked_norm.startswith(target_dir):
keys_to_unlock.append(locked_path)
for file_path in keys_to_unlock:
fd, lock_size = self.virus_lock[file_path]
for file_path in keys_to_unlock:
fd, lock_size = self.virus_lock[file_path]
try:
msvcrt.locking(fd, msvcrt.LK_UNLCK, lock_size)
except Exception as e:
if not quiet:
self.write_log("WARN", "unlock_file", source=file_path, detail=str(e), success=False)
finally:
try:
msvcrt.locking(fd, msvcrt.LK_UNLCK, lock_size)
except Exception as e:
if not quiet:
self.write_log("WARN", "unlock_file", source=file_path, detail=str(e), success=False)
os.close(fd)
except Exception:
pass
del self.virus_lock[file_path]
finally:
try:
os.close(fd)
except Exception:
pass
del self.virus_lock[file_path]
return True
def relock_file(self):
while True:
@@ -172,9 +293,88 @@ class ProtectMixin:
####################################################################################################
def _is_internal_physical_drive(self, drive):
drive_path = rf"\\.\PhysicalDrive{drive}"
handle = self.kernel32.CreateFileW(
drive_path,
0,
FILE_SHARE_READ | FILE_SHARE_WRITE,
None,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
None
)
if handle in (None, INVALID_HANDLE_VALUE):
return False
try:
query = STORAGE_PROPERTY_QUERY(STORAGE_DEVICE_PROPERTY, PROPERTY_STANDARD_QUERY)
header = STORAGE_DESCRIPTOR_HEADER()
bytes_returned = ctypes.wintypes.DWORD(0)
if not self.kernel32.DeviceIoControl(
handle,
IOCTL_STORAGE_QUERY_PROPERTY,
ctypes.byref(query),
ctypes.sizeof(query),
ctypes.byref(header),
ctypes.sizeof(header),
ctypes.byref(bytes_returned),
None
):
return False
minimum_size = STORAGE_DEVICE_DESCRIPTOR.BusType.offset + ctypes.sizeof(ctypes.c_int)
if bytes_returned.value < ctypes.sizeof(header) or header.Size < minimum_size or header.Size > 65536:
return False
descriptor_buffer = ctypes.create_string_buffer(header.Size)
bytes_returned.value = 0
if not self.kernel32.DeviceIoControl(
handle,
IOCTL_STORAGE_QUERY_PROPERTY,
ctypes.byref(query),
ctypes.sizeof(query),
descriptor_buffer,
ctypes.sizeof(descriptor_buffer),
ctypes.byref(bytes_returned),
None
):
return False
descriptor = ctypes.cast(descriptor_buffer, ctypes.POINTER(STORAGE_DEVICE_DESCRIPTOR)).contents
if bytes_returned.value < minimum_size or descriptor.RemovableMedia or descriptor.BusType not in INTERNAL_STORAGE_BUS_TYPES:
return False
hotplug = STORAGE_HOTPLUG_INFO()
hotplug.Size = ctypes.sizeof(hotplug)
bytes_returned.value = 0
if not self.kernel32.DeviceIoControl(
handle,
IOCTL_STORAGE_GET_HOTPLUG_INFO,
None,
0,
ctypes.byref(hotplug),
ctypes.sizeof(hotplug),
ctypes.byref(bytes_returned),
None
):
return False
if bytes_returned.value < ctypes.sizeof(hotplug):
return False
return not (hotplug.MediaRemovable or hotplug.MediaHotplug or hotplug.DeviceHotplug)
except Exception:
return False
finally:
self.kernel32.CloseHandle(handle)
def backup_mbr(self, max_drives=26):
self.mbr_backup = {}
for drive in range(max_drives):
if not self._is_internal_physical_drive(drive):
continue
try:
with open(rf"\\.\PhysicalDrive{drive}", "rb") as f:
mbr = f.read(512)
@@ -448,7 +648,7 @@ class ProtectMixin:
self.exist_process = cur
for pid in new_pids:
h = self.kernel32.OpenProcess(0x1F0FFF, False, pid)
h = self.kernel32.OpenProcess(PYAS_PROCESS_SCAN_ACCESS, False, pid)
if h:
with self.lock_config:
should_suspend = self.pyas_config.get("suspend_switch", True)
@@ -476,7 +676,7 @@ class ProtectMixin:
suspended = self.pyas_config.get("suspend_switch", True)
if not h:
h = self.kernel32.OpenProcess(0x1F0FFF, False, pid)
h = self.kernel32.OpenProcess(PYAS_PROCESS_SCAN_ACCESS, False, pid)
if not h:
return
@@ -578,7 +778,7 @@ class ProtectMixin:
raw_path = self.device_path_to_drive(raw_path)
file_path = self.norm_path(raw_path)
if file_path and file_path not in scanned_paths:
if file_path and file_path not in scanned_paths and self.path_system not in file_path:
scanned_paths.add(file_path)
file_path_lower = file_path.lower()
@@ -648,7 +848,7 @@ class ProtectMixin:
suffix = self.pyas_config.get("suffix", [])
if not ext_filter or os.path.splitext(file_path)[-1].lower() in suffix:
self.protect_pool.submit(self.handle_new_file, file_path)
self._queue_file_scan(file_path)
if notify.NextEntryOffset == 0:
break
@@ -666,33 +866,106 @@ class ProtectMixin:
pass
self.h_dir_file = None
def _cancel_pending_file_tasks(self, task_name=None, target_path=None):
target_norm = os.path.normcase(target_path) if target_path else None
target_dir = target_norm + os.sep if target_norm else None
with self.lock_file_ops:
task_keys = []
for task_key in self.file_task_timers:
name, path_key = task_key
if task_name and name != task_name:
continue
if target_norm and path_key != target_norm and not path_key.startswith(target_dir):
continue
task_keys.append(task_key)
timers = [self.file_task_timers.pop(task_key) for task_key in task_keys]
for task_key in task_keys:
self.file_task_generations.pop(task_key, None)
for timer in timers:
timer.cancel()
def _cancel_pending_file_scans(self):
self._cancel_pending_file_tasks("scan")
def _schedule_file_task(self, task_name, file_path, callback, delay):
task_key = (task_name, os.path.normcase(file_path))
with self.lock_file_ops:
previous_timer = self.file_task_timers.get(task_key)
if previous_timer:
previous_timer.cancel()
generation = self.file_task_generations.get(task_key, 0) + 1
timer = threading.Timer(delay, self._run_file_task, args=(task_key, file_path, generation, callback))
timer.daemon = True
self.file_task_timers[task_key] = timer
self.file_task_generations[task_key] = generation
timer.start()
def _run_file_task(self, task_key, file_path, generation, callback):
with self.lock_file_ops:
if self.file_task_generations.get(task_key) != generation:
return
if self.file_task_timers.get(task_key) is threading.current_thread():
self.file_task_timers.pop(task_key, None)
try:
callback(file_path)
finally:
with self.lock_file_ops:
if self.file_task_generations.get(task_key) == generation:
self.file_task_timers.pop(task_key, None)
self.file_task_generations.pop(task_key, None)
def _queue_file_scan(self, file_path, delay=FILE_SCAN_DEBOUNCE_SECONDS):
self._schedule_file_task("scan", file_path, self.handle_new_file, delay)
def _acquire_file_scan_gate(self, file_path):
ctypes.set_last_error(0)
handle = self.kernel32.CreateFileW(
file_path,
GENERIC_READ,
FILE_SHARE_READ,
None,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
None
)
if handle == INVALID_HANDLE_VALUE:
return None, ctypes.get_last_error()
return handle, 0
def handle_new_file(self, file_path):
try:
for _ in range(5):
try:
fd = os.open(file_path, os.O_RDONLY | os.O_BINARY)
os.close(fd)
with self.lock_config:
if not self.pyas_config.get("document_switch", False):
return
size = os.path.getsize(file_path)
if size > 0:
break
except Exception:
pass
time.sleep(0.1)
else:
gate_handle, error = self._acquire_file_scan_gate(file_path)
if not gate_handle:
if error in (ERROR_SHARING_VIOLATION, ERROR_LOCK_VIOLATION):
self._queue_file_scan(file_path, FILE_SCAN_RETRY_SECONDS)
return
with self.lock_file_ops:
if file_path in self.virus_lock: return
try:
with self.lock_file_ops:
if file_path in self.virus_lock:
return
result = self.safe_scan_engine(file_path)
self.cloud_check(file_path)
if result:
if self.manage_named_list("quarantine", [file_path], action="add", lock_func=self.lock_file) > 0:
self.write_log("BLOCK", "File Block", source=file_path, file_hash=self.calc_file_hash(file_path))
result = self.safe_scan_engine(file_path)
self.cloud_check(file_path)
if result:
if self.manage_named_list("quarantine", [file_path], action="add", lock_func=self.lock_file) > 0:
self.write_log("BLOCK", "File Block", source=file_path, file_hash=self.calc_file_hash(file_path))
finally:
self.kernel32.CloseHandle(gate_handle)
except Exception:
pass
@@ -726,7 +999,7 @@ class ProtectMixin:
def handle_new_connection(self, key):
pid, remote_addr, remote_port = key
try:
h = self.kernel32.OpenProcess(0x1F0FFF, False, pid)
h = self.kernel32.OpenProcess(PYAS_PROCESS_NETWORK_ACCESS, False, pid)
if not h:
return
@@ -768,7 +1041,10 @@ class ProtectMixin:
running = any(exe_name.lower() == "explorer.exe" for _, exe_name in self._enum_processes())
if not running:
subprocess.Popen("explorer.exe", shell=True)
explorer = self._find_windows_tool("explorer.exe")
if not explorer:
return
subprocess.Popen([explorer])
self.write_log("INFO", "System Restart", source="explorer.exe")
except Exception:
@@ -940,6 +1216,46 @@ class ProtectMixin:
self.advapi32.CloseServiceHandle(service)
self.advapi32.CloseServiceHandle(scm)
def _get_driver_client_image_path(self):
buffer = ctypes.create_unicode_buffer(32768)
size = ctypes.wintypes.DWORD(len(buffer))
if not self.kernel32.QueryFullProcessImageNameW(
self.kernel32.GetCurrentProcess(),
0,
buffer,
ctypes.byref(size)
):
return None
image_path = os.path.abspath(buffer.value)
drive, tail = os.path.splitdrive(image_path)
if not drive or not tail:
return None
device_buffer = ctypes.create_unicode_buffer(32768)
if not self.kernel32.QueryDosDeviceW(drive, device_buffer, len(device_buffer)):
return None
return device_buffer.value.rstrip("\\") + tail
def _configure_driver_identity(self):
image_path = self._get_driver_client_image_path()
if not image_path:
return False, ctypes.get_last_error() or 1
try:
access = winreg.KEY_SET_VALUE | getattr(winreg, "KEY_WOW64_64KEY", 0)
with winreg.CreateKeyEx(
winreg.HKEY_LOCAL_MACHINE,
r"SYSTEM\CurrentControlSet\Services\PYAS_Driver",
0,
access
) as key:
winreg.SetValueEx(key, "ClientImagePath", 0, winreg.REG_SZ, image_path)
return True, 0
except OSError as e:
return False, int(getattr(e, "winerror", 0) or 1)
def _ensure_driver_service(self):
desired_service_access = 0x0002 | 0x0004 | 0x0010 | 0x00010000
dependencies = ctypes.create_unicode_buffer("FltMgr\0\0")
@@ -1024,36 +1340,59 @@ class ProtectMixin:
self.advapi32.CloseServiceHandle(service)
self.advapi32.CloseServiceHandle(scm)
def _delete_driver_service(self):
scm = self.advapi32.OpenSCManagerW(None, None, 0x0001)
if not scm:
return False, ctypes.get_last_error()
def uninstall_system_driver(self):
if not self.stop_system_driver():
return False, 1051
return self._delete_driver_service()
service = None
try:
service = self.advapi32.OpenServiceW(scm, "PYAS_Driver", 0x0004 | 0x00010000)
if not service:
error = ctypes.get_last_error()
return (True, 0) if error in (1060, 1072) else (False, error)
def _delete_driver_service(self, timeout=15.0):
deadline = time.monotonic() + timeout
last_error = 0
delete_requested = False
state, error = self._query_service_state_handle(service)
if error:
return False, error
if state in (2, 3, 4):
return False, 1051
while time.monotonic() < deadline:
scm = self.advapi32.OpenSCManagerW(None, None, 0x0001)
if not scm:
return False, ctypes.get_last_error()
ctypes.set_last_error(0)
if self.advapi32.DeleteService(service):
return True, 0
service = None
try:
ctypes.set_last_error(0)
service = self.advapi32.OpenServiceW(scm, "PYAS_Driver", 0x0004 | 0x00010000)
if not service:
last_error = ctypes.get_last_error()
if last_error == 1060:
return True, 0
if last_error != 1072:
return False, last_error
else:
state, status_error = self._query_service_state_handle(service)
if status_error:
return False, status_error
error = ctypes.get_last_error()
if error in (1060, 1072):
return True, 0
return False, error
finally:
if service:
self.advapi32.CloseServiceHandle(service)
self.advapi32.CloseServiceHandle(scm)
if state not in (2, 3, 4) and not delete_requested:
ctypes.set_last_error(0)
if self.advapi32.DeleteService(service):
delete_requested = True
last_error = 1072
else:
last_error = ctypes.get_last_error()
if last_error == 1060:
return True, 0
if last_error == 1072:
delete_requested = True
else:
return False, last_error
elif state in (2, 3, 4):
last_error = 1051
finally:
if service:
self.advapi32.CloseServiceHandle(service)
self.advapi32.CloseServiceHandle(scm)
time.sleep(0.1)
return False, last_error or 1072
def install_system_driver(self):
try:
@@ -1062,6 +1401,11 @@ class ProtectMixin:
self.write_log("WARN", "Driver Service", detail=f"CreateServiceW/ChangeServiceConfigW failed: 0x{service_error & 0xFFFFFFFF:08X}", success=False)
return False
configured, identity_error = self._configure_driver_identity()
if not configured:
self.write_log("WARN", "Driver Identity", detail=f"ClientImagePath configuration failed: 0x{identity_error & 0xFFFFFFFF:08X}", success=False)
return False
self._reg_write(winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Services\PYAS_Driver\Instances", "DefaultInstance", winreg.REG_SZ, "PYAS Instance")
self._reg_write(winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Services\PYAS_Driver\Instances\PYAS Instance", "Altitude", winreg.REG_SZ, "320000")
self._reg_write(winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Services\PYAS_Driver\Instances\PYAS Instance", "Flags", winreg.REG_DWORD, 0)
@@ -1120,12 +1464,18 @@ class ProtectMixin:
return None
def _connect_driver_port(self, asynchronous=False):
context = PYAS_CONNECTION_CONTEXT()
context.Size = ctypes.sizeof(context)
context.Version = PYAS_CONNECTION_VERSION
context.Magic = PYAS_CONNECTION_MAGIC
context.ProcessId = os.getpid()
temp_port = ctypes.wintypes.HANDLE()
status = self.fltlib.FilterConnectCommunicationPort(
"\\PYAS_Output_Pipe",
0 if asynchronous else FLT_PORT_FLAG_SYNC_HANDLE,
None,
0,
ctypes.byref(context),
ctypes.sizeof(context),
None,
ctypes.byref(temp_port)
)
@@ -1526,8 +1876,12 @@ class ProtectMixin:
self._resume_driver_listener()
def check_system_driver(self):
state = self._query_driver_service_state()
return state in (2, 3, 4)
runtime_state = self._query_driver_state()
if runtime_state in (1, 2, 3, 4):
return True
service_state = self._query_driver_service_state()
return service_state in (2, 3, 4)
def clear_driver_rules(self):
with self.lock_driver:
@@ -1704,7 +2058,7 @@ class ProtectMixin:
if self.driver_stop_event.is_set():
break
if isinstance(item, dict) and item.get("file"):
self.sync_driver_whitelist(item["file"], True)
self.sync_driver_whitelist(item["file"], True, item.get("is_dir"))
if self.driver_stop_event.is_set():
break
@@ -1752,10 +2106,12 @@ class ProtectMixin:
if self.driver_stop_event.is_set():
break
self.driver_stop_event.wait(0.02)
except Exception as e:
self.driver_listener_failed_event.set()
if not self.driver_stop_event.is_set():
self.write_log("WARN", "pipe_server_thread", detail=str(e), success=False)
finally:
if owned_port:
try:
@@ -1771,4 +2127,3 @@ class ProtectMixin:
with self.lock_driver:
if self.driver_listener_thread is threading.current_thread():
self.driver_listener_thread = None
+229 -96
View File
@@ -159,25 +159,92 @@ class ScannerMixin:
####################################################################################################
def start_scan(self, targets):
with self.lock_virus:
if getattr(self, 'scan_running', False):
return
def _scan_result_messages(self, count=None, scanned=None, elapsed=None):
if count is None or scanned is None or elapsed is None:
with self.lock_virus:
count = len(getattr(self, 'virus_results', [])) if count is None else count
scanned = getattr(self, 'scan_count', 0) if scanned is None else scanned
scan_start = getattr(self, 'scan_start', time.time())
elapsed = int(time.time() - scan_start) if elapsed is None else elapsed
return {
"traditional_switch": f"發現 {count} 個病毒,掃描 {scanned} 個檔案,耗時 {elapsed} 秒",
"simplified_switch": f"发现 {count} 个病毒,扫描 {scanned} 个文件,耗时 {elapsed} 秒",
"english_switch": f"Found {count} viruses, scanned {scanned} files, time {elapsed}s",
"japanese_switch": f"{count} 個のウイルスを発見し、{scanned} 個のファイルをスキャンしました(所要時間 {elapsed} 秒)",
"korean_switch": f"바이러스 {count}개 발견, 파일 {scanned}개 검사, 소요 시간 {elapsed}초",
"french_switch": f"{count} virus trouvés, {scanned} fichiers analysés, temps {elapsed}s",
"spanish_switch": f"Se encontraron {count} virus, {scanned} archivos escaneados, tiempo {elapsed}s",
"hindi_switch": f"{count} वायरस मिले, {scanned} फ़ाइलें स्कैन की गईं, समय {elapsed}s",
"arabic_switch": f"تم العثور على {count} فيروسات، تم فحص {scanned} ملفات، الوقت {elapsed} ثانية",
"russian_switch": f"Найдено {count} вирусов, проверено {scanned} файлов, время {elapsed}с",
"slovenian_switch": f"Najdenih {count} virusov, skeniranih {scanned} datotek, čas {elapsed}s"
}
def _is_scan_cancel_requested(self):
with self.lock_virus:
return getattr(self, 'scan_stop_requested', False)
def _finish_scan_cancelled(self, messages=None):
with self.lock_virus:
count = len(getattr(self, 'virus_results', []))
scanned = getattr(self, 'scan_count', 0)
scan_start = getattr(self, 'scan_start', time.time())
elapsed = int(time.time() - scan_start)
self.scan_running = False
self.scan_preparing = False
self.scan_finished = True
self.scan_stop_requested = False
messages = messages or self._scan_result_messages(count, scanned, elapsed)
if self._window:
js_cmd = f"if(window.finishScan) window.finishScan({json.dumps(messages)}, {count});"
self.ui_queue.put(js_cmd)
return False
def _begin_scan_preparation(self):
with self.lock_virus:
if getattr(self, 'scan_running', False) or getattr(self, 'scan_preparing', False):
return False
self.scan_preparing = True
self.scan_stop_requested = False
self.scan_finished = False
self.virus_results = []
self.scan_count = 0
self.scan_start = time.time()
return True
def start_scan(self, targets, from_preparing=False):
with self.lock_virus:
if getattr(self, 'scan_running', False) or (getattr(self, 'scan_preparing', False) and not from_preparing):
return False
if getattr(self, 'scan_stop_requested', False):
self.scan_running = False
self.scan_preparing = False
self.scan_finished = True
return False
self.scan_preparing = False
self.scan_running = True
self.scan_finished = False
self.scan_stop_requested = False
self.virus_results = []
self.scan_count = 0
self.scan_start = time.time()
self.scan_pool.submit(self.scan_worker, targets)
return True
def scan_worker(self, targets):
last_update = 0.0
try:
for file_path, is_explicit in self.yield_files(targets):
with self.lock_virus:
if not self.scan_running:
if not self.scan_running or getattr(self, 'scan_stop_requested', False):
break
norm_path = self.norm_path(file_path)
@@ -203,6 +270,8 @@ class ScannerMixin:
continue
with self.lock_virus:
if not self.scan_running or getattr(self, 'scan_stop_requested', False):
break
self.scan_count += 1
with self.lock_config:
@@ -213,6 +282,10 @@ class ScannerMixin:
continue
result = self.safe_scan_engine(norm_path)
with self.lock_virus:
if not self.scan_running or getattr(self, 'scan_stop_requested', False):
break
if result:
with self.lock_virus:
self.virus_results.append(norm_path)
@@ -233,116 +306,180 @@ class ScannerMixin:
finally:
with self.lock_virus:
cancelled = getattr(self, 'scan_stop_requested', False)
self.scan_running = False
self.scan_preparing = False
self.scan_finished = True
self.scan_stop_requested = False
count, scanned, elapsed = len(self.virus_results), self.scan_count, int(time.time() - self.scan_start)
messages = {
"traditional_switch": f"發現 {count} 個病毒,掃描 {scanned} 個檔案,耗時 {elapsed} 秒",
"simplified_switch": f"发现 {count} 个病毒,扫描 {scanned} 个文件,耗时 {elapsed} 秒",
"english_switch": f"Found {count} viruses, scanned {scanned} files, time {elapsed}s",
"japanese_switch": f"{count} 個のウイルスを発見し、{scanned} 個のファイルをスキャンしました(所要時間 {elapsed} 秒)",
"korean_switch": f"바이러스 {count}개 발견, 파일 {scanned}개 검사, 소요 시간 {elapsed}초",
"french_switch": f"{count} virus trouvés, {scanned} fichiers analysés, temps {elapsed}s",
"spanish_switch": f"Se encontraron {count} virus, {scanned} archivos escaneados, tiempo {elapsed}s",
"hindi_switch": f"{count} वायरस मिले, {scanned} फ़ाइलें स्कैन की गईं, समय {elapsed}s",
"arabic_switch": f"تم العثور على {count} فيروسات، تم فحص {scanned} ملفات، الوقت {elapsed} ثانية",
"russian_switch": f"Найдено {count} вирусов, проверено {scanned} файлов, время {elapsed}с",
"slovenian_switch": f"Najdenih {count} virusov, skeniranih {scanned} datotek, čas {elapsed}s"
}
messages = self._scan_result_messages(count, scanned, elapsed)
log_detail = f"Found {count} viruses, scanned {scanned} files, time {elapsed}s"
if self._window:
js_cmd = f"if(window.finishScan) window.finishScan({json.dumps(messages)}, {count});"
self.ui_queue.put(js_cmd)
self.write_log("INFO", "Scan Completed", detail=f"Found {count} viruses, scanned {scanned} files, time {elapsed}s")
self.write_log("INFO", "Scan Completed", detail=log_detail)
####################################################################################################
def stop_scan(self):
with self.lock_virus: self.scan_running = False
with self.lock_virus:
active = getattr(self, 'scan_running', False) or getattr(self, 'scan_preparing', False)
if active:
self.scan_stop_requested = True
self.scan_running = False
return active
def trigger_scan(self, method):
messages = {
"traditional_switch": "已取消掃描",
"simplified_switch": "已取消扫描", "english_switch":
"Scan cancelled", "japanese_switch": "スキャンがキャンセルされました",
"korean_switch": "스캔이 취소되었습니다",
"french_switch": "Analyse annulée",
"spanish_switch": "Escaneo cancelado",
"hindi_switch": "स्कैन रद्द कर दिया गया",
"arabic_switch": "تم إلغاء الفحص",
"russian_switch": "Сканирование отменено",
"slovenian_switch": "Skeniranje preklicano"
}
targets = []
if method == "smart":
for folder in ["Desktop", "Downloads", "AppData"]:
fp = os.path.join(self.path_user, folder)
if os.path.exists(fp):
targets.append(fp)
if not self._begin_scan_preparation():
return False
if os.path.exists(self.path_config):
targets.append(self.path_config)
try:
if method == "smart":
for folder in ["Desktop", "Downloads", "AppData"]:
if self._is_scan_cancel_requested():
return self._finish_scan_cancelled()
buf = ctypes.create_unicode_buffer(1024)
max_address = 0x7FFFFFFFFFFF if ctypes.sizeof(ctypes.c_void_p) == 8 else 0x7FFFFFFF
system_dir = self.path_system.lower()
fp = os.path.join(self.path_user, folder)
if os.path.exists(fp):
targets.append(fp)
for proc in self.get_process_list():
if proc["path"] and proc["path"] != "None":
targets.append(proc["path"])
if self._is_scan_cancel_requested():
return self._finish_scan_cancelled()
pid = proc["pid"]
if pid <= 4:
continue
if os.path.exists(self.path_config):
targets.append(self.path_config)
h_process = self.kernel32.OpenProcess(0x1000, False, pid)
if h_process:
try:
address = 0
mbi = MEMORY_BASIC_INFORMATION()
while address < max_address and self.kernel32.VirtualQueryEx(h_process, ctypes.c_void_p(address), ctypes.byref(mbi), ctypes.sizeof(mbi)):
if mbi.State == 0x1000 and mbi.Type == 0x1000000:
if self.psapi.GetMappedFileNameW(h_process, ctypes.c_void_p(address), buf, 1024):
raw_path = buf.value
if raw_path.startswith("\\"):
raw_path = self.device_path_to_drive(raw_path)
buf = ctypes.create_unicode_buffer(1024)
max_address = 0x7FFFFFFFFFFF if ctypes.sizeof(ctypes.c_void_p) == 8 else 0x7FFFFFFF
system_dir = self.path_system.lower()
file_path = self.norm_path(raw_path)
if file_path and not file_path.lower().startswith(system_dir):
targets.append(file_path)
for proc in self.get_process_list():
if self._is_scan_cancel_requested():
return self._finish_scan_cancelled()
if mbi.RegionSize == 0:
break
address += mbi.RegionSize
finally:
self.kernel32.CloseHandle(h_process)
if proc["path"] and proc["path"] != "None":
targets.append(proc["path"])
self.start_scan(list(set(targets)))
pid = proc["pid"]
if pid <= 4:
continue
elif method == "file":
targets = self.select_files()
if targets:
self.start_scan(targets)
h_process = self.kernel32.OpenProcess(0x1000, False, pid)
if h_process:
try:
address = 0
mbi = MEMORY_BASIC_INFORMATION()
while address < max_address and not self._is_scan_cancel_requested() and self.kernel32.VirtualQueryEx(h_process, ctypes.c_void_p(address), ctypes.byref(mbi), ctypes.sizeof(mbi)):
elif self._window:
self._window.evaluate_js(f"if(window.finishScan) window.finishScan({json.dumps(messages)}, 0);")
if mbi.State == 0x1000 and mbi.Type == 0x1000000:
if self.psapi.GetMappedFileNameW(h_process, ctypes.c_void_p(address), buf, 1024):
raw_path = buf.value
if raw_path.startswith("\\"):
raw_path = self.device_path_to_drive(raw_path)
elif method == "path":
targets = self.select_folder()
if targets:
self.start_scan(targets)
file_path = self.norm_path(raw_path)
if file_path and not file_path.lower().startswith(system_dir):
targets.append(file_path)
elif self._window:
self._window.evaluate_js(f"if(window.finishScan) window.finishScan({json.dumps(messages)}, 0);")
if mbi.RegionSize == 0:
break
address += mbi.RegionSize
finally:
self.kernel32.CloseHandle(h_process)
elif method == "full":
targets = [f"{chr(d)}:/" for d in range(65, 91) if os.path.exists(f"{chr(d)}:/")]
self.start_scan(targets)
if self._is_scan_cancel_requested():
return self._finish_scan_cancelled()
if not self.start_scan(list(set(targets)), from_preparing=True):
return self._finish_scan_cancelled()
return True
elif method == "file":
targets = self.select_files()
if self._is_scan_cancel_requested():
return self._finish_scan_cancelled()
if targets:
if not self.start_scan(targets, from_preparing=True):
return self._finish_scan_cancelled()
return True
return self._finish_scan_cancelled()
elif method == "path":
targets = self.select_folder()
if self._is_scan_cancel_requested():
return self._finish_scan_cancelled()
if targets:
if not self.start_scan(targets, from_preparing=True):
return self._finish_scan_cancelled()
return True
return self._finish_scan_cancelled()
elif method == "full":
targets = [f"{chr(d)}:/" for d in range(65, 91) if os.path.exists(f"{chr(d)}:/")]
if self._is_scan_cancel_requested() or not self.start_scan(targets, from_preparing=True):
return self._finish_scan_cancelled()
return True
return self._finish_scan_cancelled()
except Exception as e:
self.write_log("WARN", "trigger_scan", detail=str(e), success=False)
return self._finish_scan_cancelled()
####################################################################################################
def _queue_virus_delete(self, file_path):
self._schedule_file_task("delete", file_path, self._retry_virus_delete, 0.5)
def _retry_virus_delete(self, file_path):
file_hash = self.calc_file_hash(file_path)
try:
with self.lock_file_ops:
if file_path in self.virus_lock:
self.lock_file(file_path, False)
target_key = os.path.normcase(self.norm_path(file_path, must_exist=False) or file_path)
for proc in self.get_process_list():
proc_path = proc.get("path")
if not proc_path or proc_path == "None":
continue
proc_key = os.path.normcase(self.norm_path(proc_path, must_exist=False) or proc_path)
if proc_key == target_key:
self.kill_process(proc["pid"])
try:
os.chmod(file_path, stat.S_IWRITE)
except Exception:
pass
os.remove(file_path)
except FileNotFoundError:
pass
except Exception as e:
self.lock_file(file_path, True, quiet=True)
self._queue_virus_delete(file_path)
self.write_log("INFO", "Virus Delete Deferred", source=file_path, detail=str(e))
return
self.remove_list_items("quarantine", [file_path])
with self.lock_virus:
target_key = os.path.normcase(self.norm_path(file_path, must_exist=False) or file_path)
self.virus_results = [
result for result in self.virus_results
if os.path.normcase(self.norm_path(result, must_exist=False) or result) != target_key
]
self.write_log("INFO", "Virus Delete", source=file_path, file_hash=file_hash, operate=True, success=True)
def solve_scan(self, file_paths):
deleted_paths = []
proc_map = {}
@@ -383,23 +520,19 @@ class ScannerMixin:
except Exception:
pass
delete_success = False
for _ in range(5):
try:
os.remove(path)
delete_success = True
break
except Exception:
time.sleep(0.1)
if not delete_success:
file_hash = self.calc_file_hash(path)
try:
os.remove(path)
except Exception as e:
self.lock_file(path, True, quiet=True)
self._queue_virus_delete(path)
self.write_log("INFO", "Virus Delete Deferred", source=path, detail=str(e))
continue
self.remove_list_items("quarantine", [path])
deleted_paths.append(raw_path)
deleted_paths.append(raw_path)
deleted_set.add(path)
self.write_log("INFO", "Virus Delete", source=path, file_hash=self.calc_file_hash(path), operate=True, success=True)
self.write_log("INFO", "Virus Delete", source=path, file_hash=file_hash, operate=True, success=True)
except Exception as e:
self.lock_file(path, True)
+345 -44
View File
@@ -1,4 +1,4 @@
import os, re, io, csv, time, shutil, threading
import os, re, io, csv, json, time, shutil, threading
import pefile, hashlib, winreg, requests, subprocess
import ctypes, ctypes.wintypes
@@ -6,6 +6,13 @@ from concurrent.futures import ThreadPoolExecutor
####################################################################################################
PROCESS_TERMINATE = 0x0001
PROCESS_QUERY_INFORMATION = 0x0400
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
PYAS_PROCESS_TERMINATE_ACCESS = PROCESS_TERMINATE | PROCESS_QUERY_INFORMATION | PROCESS_QUERY_LIMITED_INFORMATION
####################################################################################################
class PROCESSENTRY32W(ctypes.Structure):
_fields_ = [
("dwSize", ctypes.wintypes.DWORD),
@@ -162,6 +169,187 @@ class RECT(ctypes.Structure):
####################################################################################################
class ToolsMixin:
def _find_windows_tool(self, *names):
windows_dir = os.environ.get("SystemRoot") or os.environ.get("WINDIR") or r"C:\Windows"
search_dirs = [
os.path.join(windows_dir, "Sysnative"),
os.path.join(windows_dir, "System32"),
windows_dir
]
for name in names:
executable_name = name if name.lower().endswith(".exe") else name + ".exe"
for directory in search_dirs:
candidate = os.path.join(directory, executable_name)
if os.path.isfile(candidate):
return candidate
candidate = shutil.which(name)
if candidate:
return candidate
return None
def _run_windows_tool(self, tool_names, arguments, **kwargs):
executable = self._find_windows_tool(*tool_names)
if not executable:
return None
options = {"creationflags": 0x08000000}
options.update(kwargs)
try:
return subprocess.run([executable, *arguments], **options)
except (FileNotFoundError, OSError):
return None
def _run_powershell(self, command, **kwargs):
return self._run_windows_tool(
("powershell.exe", "pwsh.exe"),
["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", command],
**kwargs
)
def _set_registry_autostart(self, enable):
run_key = r"Software\Microsoft\Windows\CurrentVersion\Run"
value_name = "PYAS_Security"
if enable:
command = f'"{self.file_pyas}" -hide'
return self._reg_write(winreg.HKEY_CURRENT_USER, run_key, value_name, winreg.REG_SZ, command)
try:
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, run_key, 0, winreg.KEY_SET_VALUE) as reg:
try:
winreg.DeleteValue(reg, value_name)
except FileNotFoundError:
pass
return True
except FileNotFoundError:
return True
except Exception:
return False
def _clear_event_log(self, log_name):
result = self._run_windows_tool(
("wevtutil.exe",),
["cl", log_name],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
if result is not None and result.returncode == 0:
return True
try:
wevtapi = ctypes.WinDLL("wevtapi", use_last_error=True)
wevtapi.EvtClearLog.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.LPCWSTR, ctypes.wintypes.LPCWSTR, ctypes.wintypes.DWORD]
wevtapi.EvtClearLog.restype = ctypes.wintypes.BOOL
return bool(wevtapi.EvtClearLog(None, log_name, None, 0))
except Exception:
return False
def _manage_service_native(self, service_name, action):
SC_MANAGER_CONNECT = 0x0001
SERVICE_CHANGE_CONFIG = 0x0002
DELETE = 0x00010000
SERVICE_NO_CHANGE = 0xFFFFFFFF
SERVICE_AUTO_START = 0x00000002
SERVICE_DISABLED = 0x00000004
try:
advapi32 = ctypes.WinDLL("advapi32", use_last_error=True)
advapi32.OpenSCManagerW.argtypes = [ctypes.wintypes.LPCWSTR, ctypes.wintypes.LPCWSTR, ctypes.wintypes.DWORD]
advapi32.OpenSCManagerW.restype = ctypes.wintypes.HANDLE
advapi32.OpenServiceW.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.LPCWSTR, ctypes.wintypes.DWORD]
advapi32.OpenServiceW.restype = ctypes.wintypes.HANDLE
advapi32.CloseServiceHandle.argtypes = [ctypes.wintypes.HANDLE]
advapi32.CloseServiceHandle.restype = ctypes.wintypes.BOOL
manager = advapi32.OpenSCManagerW(None, None, SC_MANAGER_CONNECT)
if not manager:
return False
service = None
try:
desired_access = DELETE if action == "delete" else SERVICE_CHANGE_CONFIG
service = advapi32.OpenServiceW(manager, service_name, desired_access)
if not service:
return False
if action == "delete":
advapi32.DeleteService.argtypes = [ctypes.wintypes.HANDLE]
advapi32.DeleteService.restype = ctypes.wintypes.BOOL
return bool(advapi32.DeleteService(service))
start_type = SERVICE_AUTO_START if action == "enable" else SERVICE_DISABLED
advapi32.ChangeServiceConfigW.argtypes = [
ctypes.wintypes.HANDLE,
ctypes.wintypes.DWORD,
ctypes.wintypes.DWORD,
ctypes.wintypes.DWORD,
ctypes.wintypes.LPCWSTR,
ctypes.wintypes.LPCWSTR,
ctypes.POINTER(ctypes.wintypes.DWORD),
ctypes.wintypes.LPCWSTR,
ctypes.wintypes.LPCWSTR,
ctypes.wintypes.LPCWSTR,
ctypes.wintypes.LPCWSTR
]
advapi32.ChangeServiceConfigW.restype = ctypes.wintypes.BOOL
return bool(advapi32.ChangeServiceConfigW(
service,
SERVICE_NO_CHANGE,
start_type,
SERVICE_NO_CHANGE,
None,
None,
None,
None,
None,
None,
None
))
finally:
if service:
advapi32.CloseServiceHandle(service)
advapi32.CloseServiceHandle(manager)
except Exception:
return False
def _manage_scheduled_task(self, task_path, action):
schtasks_action = "/delete" if action == "delete" else "/change"
arguments = [schtasks_action, "/tn", task_path]
if action == "delete":
arguments.append("/f")
else:
arguments.append("/enable" if action == "enable" else "/disable")
result = self._run_windows_tool(
("schtasks.exe",),
arguments,
capture_output=True,
text=True
)
if result is not None and result.returncode == 0:
return True
escaped_path = task_path.replace("'", "''")
operation = {
"delete": "Unregister-ScheduledTask -InputObject $Task -Confirm:$false",
"enable": "Enable-ScheduledTask -InputObject $Task | Out-Null",
"disable": "Disable-ScheduledTask -InputObject $Task | Out-Null"
}.get(action)
if not operation:
return False
command = (
f"$FullName = '{escaped_path}'; "
"$Task = Get-ScheduledTask -ErrorAction SilentlyContinue | "
"Where-Object { ($_.TaskPath + $_.TaskName) -eq $FullName } | Select-Object -First 1; "
f"if ($Task) {{ {operation} }} else {{ exit 1 }}"
)
result = self._run_powershell(command, capture_output=True, text=True)
return bool(result and result.returncode == 0)
def _reg_read(self, root, path, value_name):
try:
with winreg.OpenKey(root, path, 0, winreg.KEY_READ) as reg:
@@ -198,18 +386,81 @@ class ToolsMixin:
####################################################################################################
def manage_autostart(self, enable):
try:
task_name = "PYAS_Security_ATS"
if enable:
cmd = f"$Action = New-ScheduledTaskAction -Execute '{self.file_pyas}' -Argument '-hide'; $Trigger = New-ScheduledTaskTrigger -AtLogOn; $Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries; Register-ScheduledTask -TaskName '{task_name}' -Action $Action -Trigger $Trigger -Settings $Settings -RunLevel Highest -Force"
subprocess.run(["powershell", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", cmd], creationflags=0x08000000)
else:
subprocess.run(["schtasks", "/Delete", "/TN", task_name, "/F"], creationflags=0x08000000)
task_name = "PYAS_Security_ATS"
executable = os.path.normpath(self.file_pyas)
escaped_executable = executable.replace("'", "''")
errors = []
return True
except Exception as e:
self.write_log("WARN", "manage_autostart", detail=str(e), success=False)
return False
if enable:
task_command = f'"{executable}" -hide'
result = self._run_windows_tool(
("schtasks.exe",),
["/Create", "/TN", task_name, "/TR", task_command, "/SC", "ONLOGON", "/RL", "HIGHEST", "/F"],
capture_output=True,
text=True
)
if result and result.returncode == 0:
self._set_registry_autostart(False)
return True
if result:
errors.append((result.stderr or result.stdout or f"schtasks exit {result.returncode}").strip())
command = (
f"$Action = New-ScheduledTaskAction -Execute '{escaped_executable}' -Argument '-hide'; "
"$Trigger = New-ScheduledTaskTrigger -AtLogOn; "
"$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries; "
f"Register-ScheduledTask -TaskName '{task_name}' -Action $Action -Trigger $Trigger -Settings $Settings -RunLevel Highest -Force | Out-Null"
)
result = self._run_powershell(command, capture_output=True, text=True)
if result and result.returncode == 0:
self._set_registry_autostart(False)
return True
if result:
errors.append((result.stderr or result.stdout or f"PowerShell exit {result.returncode}").strip())
if self._set_registry_autostart(True):
self.write_log("INFO", "manage_autostart", detail="Scheduled Task unavailable; using HKCU Run fallback", success=True)
return True
else:
task_removed = False
query_result = self._run_windows_tool(
("schtasks.exe",),
["/Query", "/TN", task_name],
capture_output=True,
text=True
)
result = None
if query_result is not None and query_result.returncode != 0:
task_removed = True
elif query_result is not None:
result = self._run_windows_tool(
("schtasks.exe",),
["/Delete", "/TN", task_name, "/F"],
capture_output=True,
text=True
)
task_removed = bool(result and result.returncode == 0)
if query_result is None or not task_removed:
command = (
f"$Task = Get-ScheduledTask -TaskName '{task_name}' -ErrorAction SilentlyContinue; "
"if ($Task) { $Task | Unregister-ScheduledTask -Confirm:$false }"
)
powershell_result = self._run_powershell(command, capture_output=True, text=True)
if powershell_result is not None:
task_removed = powershell_result.returncode == 0
result = powershell_result
if result and result.returncode != 0:
errors.append((result.stderr or result.stdout or f"task removal exit {result.returncode}").strip())
registry_removed = self._set_registry_autostart(False)
if registry_removed and (task_removed or query_result is None and result is None):
return True
detail = "; ".join(error for error in errors if error) or "No supported autostart method was available"
self.write_log("WARN", "manage_autostart", detail=detail, success=False)
return False
####################################################################################################
@@ -287,10 +538,16 @@ class ToolsMixin:
if lock_func:
lock_func(path, True)
target_list.append({"file": path, "time": time.time()})
new_item = {"file": path, "time": time.time()}
is_directory = None
if list_key == "white_list":
is_directory = os.path.isdir(path)
new_item["is_dir"] = is_directory
target_list.append(new_item)
acted_items.append(path)
if list_key == "white_list":
self.sync_driver_whitelist(path, True)
self.sync_driver_whitelist(path, True, is_directory)
elif action == "remove":
norm_paths_case = {os.path.normcase(p) for p in norm_paths}
@@ -306,7 +563,8 @@ class ToolsMixin:
acted_items.append(val)
if list_key == "white_list":
self.sync_driver_whitelist(val, False)
is_directory = item.get("is_dir") if isinstance(item, dict) else None
self.sync_driver_whitelist(val, False, is_directory)
continue
new_list.append(item)
@@ -351,7 +609,8 @@ class ToolsMixin:
if np and os.path.normcase(np) in norm_paths_to_remove:
removed_items.append(val)
if list_key == "white_list":
self.sync_driver_whitelist(val, False)
is_directory = item.get("is_dir") if isinstance(item, dict) else None
self.sync_driver_whitelist(val, False, is_directory)
continue
new_list.append(item)
@@ -657,7 +916,7 @@ class ToolsMixin:
def kill_process(self, pid, expected_path=None):
try:
h = self.kernel32.OpenProcess(0x1F0FFF, False, pid)
h = self.kernel32.OpenProcess(PYAS_PROCESS_TERMINATE_ACCESS, False, pid)
if h:
try:
file_path = self.norm_path(self.get_process_file(h))
@@ -883,7 +1142,7 @@ class ToolsMixin:
log_path = os.path.join(self.path_system, "System32", "winevt", "Logs", f"{log_type}.evtx")
size = os.path.getsize(log_path) if os.path.exists(log_path) else 0
if subprocess.run(["wevtutil", "cl", log_type], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=0x08000000).returncode == 0:
if self._clear_event_log(log_type):
total_deleted += size
except Exception:
@@ -1030,20 +1289,57 @@ class ToolsMixin:
def get_tasks():
res = []
try:
proc = subprocess.run(["schtasks", "/query", "/fo", "csv", "/v"], capture_output=True, text=True, creationflags=0x08000000)
reader = csv.reader(io.StringIO(proc.stdout))
next(reader, None)
proc = self._run_windows_tool(
("schtasks.exe",),
["/query", "/fo", "csv", "/v"],
capture_output=True,
text=True
)
if proc is not None and proc.returncode == 0:
reader = csv.reader(io.StringIO(proc.stdout))
next(reader, None)
for row in reader:
if len(row) > 8 and row[1].strip() and row[8].strip() and row[8] != "N/A":
name = row[1].split('\\')[-1]
path = row[8]
status = row[3]
for row in reader:
if len(row) > 8 and row[1].strip() and row[8].strip() and row[8] != "N/A":
name = row[1].split('\\')[-1]
path = row[8]
status = row[3]
if path and "windows" not in path.lower():
fp = self.extract_paths_from_cmdline(path)
fpp = fp[0] if fp else path
res.append({"id": f"tsk|{row[1]}", "type": "type_tsk", "name": name, "status": "disabled" if status == "Disabled" else "enabled", "path": fpp})
if path and "windows" not in path.lower():
fp = self.extract_paths_from_cmdline(path)
fpp = fp[0] if fp else path
res.append({"id": f"tsk|{row[1]}", "type": "type_tsk", "name": name, "status": "disabled" if status.lower() == "disabled" else "enabled", "path": fpp})
return res
command = (
"[Console]::OutputEncoding = [Text.UTF8Encoding]::new(); "
"Get-ScheduledTask -ErrorAction SilentlyContinue | ForEach-Object { "
"$Action = @($_.Actions)[0]; "
"[pscustomobject]@{Name=$_.TaskName; FullName=($_.TaskPath + $_.TaskName); "
"State=[string]$_.State; Execute=$Action.Execute; Arguments=$Action.Arguments} "
"} | ConvertTo-Json -Compress"
)
proc = self._run_powershell(command, capture_output=True, text=True, encoding="utf-8", errors="replace")
if not proc or proc.returncode != 0 or not proc.stdout.strip():
return res
task_data = json.loads(proc.stdout)
if isinstance(task_data, dict):
task_data = [task_data]
for task in task_data:
name = str(task.get("Name") or "").strip()
full_name = str(task.get("FullName") or name).strip()
execute = str(task.get("Execute") or "").strip()
arguments = str(task.get("Arguments") or "").strip()
command_line = " ".join(part for part in (execute, arguments) if part)
if not name or not command_line or "windows" in command_line.lower():
continue
paths = self.extract_paths_from_cmdline(command_line)
path = paths[0] if paths else execute or command_line
status = "disabled" if str(task.get("State") or "").lower() == "disabled" else "enabled"
res.append({"id": f"tsk|{full_name}", "type": "type_tsk", "name": name, "status": status, "path": path})
except Exception:
pass
@@ -1063,44 +1359,49 @@ class ToolsMixin:
def manage_startup(self, items, action):
def process_item(item_id):
try:
parts = item_id.split('|')
parts = item_id.split('|', 3)
stype = parts[0]
if stype == "reg":
root, old_path, name = int(parts[1]), parts[2], parts[3]
if action == "delete":
self._reg_delete(root, old_path, name)
return self._reg_delete(root, old_path, name)
else:
new_path = old_path.replace("_Disabled", "") if action == "enable" else (old_path if "_Disabled" in old_path else old_path + "_Disabled")
if old_path != new_path:
val = self._reg_read(root, old_path, name)
if val:
self._reg_write(root, new_path, name, winreg.REG_SZ, val)
self._reg_delete(root, old_path, name)
return self._reg_write(root, new_path, name, winreg.REG_SZ, val) and self._reg_delete(root, old_path, name)
return True
elif stype == "srv":
name = parts[1]
if action == "delete":
subprocess.run(["sc", "delete", name], capture_output=True, creationflags=0x08000000)
result = self._run_windows_tool(("sc.exe",), ["delete", name], capture_output=True)
else:
mode = "auto" if action == "enable" else "disabled"
subprocess.run(["sc", "config", name, f"start=", mode], capture_output=True, creationflags=0x08000000)
result = self._run_windows_tool(("sc.exe",), ["config", name, "start=", mode], capture_output=True)
if result is not None and result.returncode == 0:
return True
return self._manage_service_native(name, action)
elif stype == "tsk":
name = parts[1]
if action == "delete":
subprocess.run(["schtasks", "/delete", "/tn", name, "/f"], capture_output=True, creationflags=0x08000000)
name = item_id.partition('|')[2]
return self._manage_scheduled_task(name, action)
else:
cmd = "/enable" if action == "enable" else "/disable"
subprocess.run(["schtasks", "/change", "/tn", name, cmd], capture_output=True, creationflags=0x08000000)
return False
except Exception as e:
self.write_log("WARN", "manage_startup", detail=str(e), success=False)
return False
with ThreadPoolExecutor(max_workers=5) as executor:
list(executor.map(process_item, items))
results = list(executor.map(process_item, items))
return True
success = all(results) if results else True
if not success:
self.write_log("WARN", "manage_startup", detail="One or more startup items could not be updated", success=False)
return success
+9 -8
View File
@@ -1,11 +1,12 @@
import pyinstaller_versionfile
pyinstaller_versionfile.create_versionfile(
output_file="versionfile.txt",
version='3.6.2.0',
company_name="PYAS Security",
file_description="PYAS Security Antivirus",
internal_name="PYAS",
legal_copyright="PYAS Security",
original_filename="PYAS.exe",
product_name="PYAS")
output_file="versionfile.txt",
version='3.7.0.0',
company_name="PYAS Security",
file_description="PYAS Security Antivirus",
internal_name="PYAS",
legal_copyright="PYAS Security",
original_filename="PYAS.exe",
product_name="PYAS"
)
+14 -3
View File
@@ -8,7 +8,9 @@
constexpr auto PYAS_PORT_NAME = L"\\PYAS_Output_Pipe";
constexpr auto MAX_PATH_LEN = 1024;
constexpr auto PYAS_POOL_TAG = 'SAYP';
constexpr auto PYAS_OB_ALTITUDE = L"320000.4101";
constexpr auto PYAS_OB_ALTITUDE = L"320000.4101";
constexpr auto PYAS_CONNECTION_MAGIC = 0x53415950;
constexpr auto PYAS_CONNECTION_VERSION = 1;
#ifndef PROCESS_TERMINATE
#define PROCESS_TERMINATE (0x0001)
@@ -130,7 +132,14 @@ typedef enum _PYAS_COMMAND {
typedef struct _PYAS_USER_MESSAGE {
ULONG Command;
WCHAR Path[MAX_PATH_LEN];
} PYAS_USER_MESSAGE, * PPYAS_USER_MESSAGE;
} PYAS_USER_MESSAGE, * PPYAS_USER_MESSAGE;
typedef struct _PYAS_CONNECTION_CONTEXT {
ULONG Size;
ULONG Version;
ULONG Magic;
ULONG ProcessId;
} PYAS_CONNECTION_CONTEXT, * PPYAS_CONNECTION_CONTEXT;
typedef struct _DRIVER_DATA {
PDRIVER_OBJECT DriverObject;
@@ -150,7 +159,9 @@ typedef struct _DRIVER_DATA {
volatile LONG ClientCloseActive;
EX_RUNDOWN_REF PortRundown;
KEVENT CleanupCompleteEvent;
PVOID ObRegistrationHandle;
PVOID ObRegistrationHandle;
UNICODE_STRING ClientImagePath;
WCHAR ClientImagePathBuffer[MAX_PATH_LEN];
} DRIVER_DATA, * PDRIVER_DATA;
typedef struct _RULE_NODE {
+99 -2
View File
@@ -49,6 +49,99 @@ static NTSTATUS SelectFailure(NTSTATUS CurrentStatus, NTSTATUS CandidateStatus)
return CandidateStatus;
}
static NTSTATUS LoadClientIdentityFromRegistry(PUNICODE_STRING RegistryPath) {
if (!RegistryPath || !RegistryPath->Buffer || !GlobalData.ClientImagePath.Buffer) return STATUS_INVALID_PARAMETER;
UNICODE_STRING ValueName = RTL_CONSTANT_STRING(L"ClientImagePath");
OBJECT_ATTRIBUTES ObjectAttributes;
InitializeObjectAttributes(&ObjectAttributes, RegistryPath, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
HANDLE KeyHandle = NULL;
NTSTATUS Status = ZwOpenKey(&KeyHandle, KEY_READ, &ObjectAttributes);
if (!NT_SUCCESS(Status)) return Status;
ULONG ResultLength = 0;
Status = ZwQueryValueKey(KeyHandle, &ValueName, KeyValuePartialInformation, NULL, 0, &ResultLength);
if (Status != STATUS_BUFFER_TOO_SMALL && Status != STATUS_BUFFER_OVERFLOW) {
ZwClose(KeyHandle);
return Status;
}
if (ResultLength == 0 || ResultLength > FIELD_OFFSET(KEY_VALUE_PARTIAL_INFORMATION, Data) + GlobalData.ClientImagePath.MaximumLength) {
ZwClose(KeyHandle);
return STATUS_NAME_TOO_LONG;
}
PKEY_VALUE_PARTIAL_INFORMATION Information = (PKEY_VALUE_PARTIAL_INFORMATION)PyasAllocate(ResultLength);
if (!Information) {
ZwClose(KeyHandle);
return STATUS_INSUFFICIENT_RESOURCES;
}
Status = ZwQueryValueKey(KeyHandle, &ValueName, KeyValuePartialInformation, Information, ResultLength, &ResultLength);
ZwClose(KeyHandle);
if (NT_SUCCESS(Status)) {
if ((Information->Type != REG_SZ && Information->Type != REG_EXPAND_SZ) ||
Information->DataLength < sizeof(WCHAR) ||
(Information->DataLength % sizeof(WCHAR)) != 0 ||
Information->DataLength > GlobalData.ClientImagePath.MaximumLength - sizeof(WCHAR)) {
Status = STATUS_INVALID_PARAMETER;
}
else {
RtlZeroMemory(GlobalData.ClientImagePath.Buffer, GlobalData.ClientImagePath.MaximumLength);
RtlCopyMemory(GlobalData.ClientImagePath.Buffer, Information->Data, Information->DataLength);
USHORT CharacterCount = (USHORT)(Information->DataLength / sizeof(WCHAR));
while (CharacterCount > 0 && GlobalData.ClientImagePath.Buffer[CharacterCount - 1] == L'\0') {
CharacterCount--;
}
if (CharacterCount == 0) {
Status = STATUS_INVALID_PARAMETER;
}
else {
GlobalData.ClientImagePath.Buffer[CharacterCount] = L'\0';
GlobalData.ClientImagePath.Length = CharacterCount * sizeof(WCHAR);
Status = STATUS_SUCCESS;
}
}
}
PyasFree(Information);
return Status;
}
static BOOLEAN IsAuthorizedClientConnection(PVOID ConnectionContext, ULONG SizeOfContext) {
if (!ConnectionContext || SizeOfContext < sizeof(PYAS_CONNECTION_CONTEXT)) return FALSE;
if (!GlobalData.ClientImagePath.Buffer || GlobalData.ClientImagePath.Length == 0) return FALSE;
if (KeGetCurrentIrql() != PASSIVE_LEVEL) return FALSE;
PYAS_CONNECTION_CONTEXT Context = { 0 };
__try {
RtlCopyMemory(&Context, ConnectionContext, sizeof(Context));
}
__except (EXCEPTION_EXECUTE_HANDLER) {
return FALSE;
}
if (Context.Size < sizeof(PYAS_CONNECTION_CONTEXT)) return FALSE;
if (Context.Version != PYAS_CONNECTION_VERSION) return FALSE;
if (Context.Magic != PYAS_CONNECTION_MAGIC) return FALSE;
if (Context.ProcessId != (ULONG)(ULONG_PTR)PsGetCurrentProcessId()) return FALSE;
PUNICODE_STRING ImagePath = NULL;
NTSTATUS Status = SeLocateProcessImageName(PsGetCurrentProcess(), &ImagePath);
if (!NT_SUCCESS(Status) || !ImagePath || !ImagePath->Buffer) {
if (ImagePath) ExFreePool(ImagePath);
return FALSE;
}
BOOLEAN Authorized = RtlEqualUnicodeString(ImagePath, &GlobalData.ClientImagePath, TRUE);
ExFreePool(ImagePath);
return Authorized;
}
static NTSTATUS UnregisterRuntimeCallbacksPass(BOOLEAN WaitWithoutTimeout) {
NTSTATUS Status = STATUS_SUCCESS;
@@ -434,10 +527,9 @@ static NTSTATUS PortConnect(
PVOID* ConnectionPortCookie
) {
UNREFERENCED_PARAMETER(ServerPortCookie);
UNREFERENCED_PARAMETER(ConnectionContext);
UNREFERENCED_PARAMETER(SizeOfContext);
if (!ConnectionPortCookie) return STATUS_INVALID_PARAMETER;
if (!IsAuthorizedClientConnection(ConnectionContext, SizeOfContext)) return STATUS_ACCESS_DENIED;
*ConnectionPortCookie = NULL;
if (InterlockedCompareExchange(&GlobalData.PortStopping, 0, 0) != 0) {
@@ -544,6 +636,8 @@ extern "C" NTSTATUS DriverEntry(
) {
RtlZeroMemory(&GlobalData, sizeof(GlobalData));
GlobalData.DriverObject = DriverObject;
GlobalData.ClientImagePath.Buffer = GlobalData.ClientImagePathBuffer;
GlobalData.ClientImagePath.MaximumLength = sizeof(GlobalData.ClientImagePathBuffer);
KeInitializeSpinLock(&GlobalData.PortMutex);
KeInitializeSpinLock(&GlobalData.TrackerMutex);
KeInitializeSpinLock(&GlobalData.PidLock);
@@ -567,6 +661,9 @@ extern "C" NTSTATUS DriverEntry(
Status = LoadRulesFromDisk(RegistryPath);
if (!NT_SUCCESS(Status)) goto Failure;
Status = LoadClientIdentityFromRegistry(RegistryPath);
if (!NT_SUCCESS(Status)) goto Failure;
Status = FltRegisterFilter(DriverObject, &FilterRegistration, &GlobalData.FilterHandle);
if (!NT_SUCCESS(Status)) goto Failure;
+341 -93
View File
@@ -1,16 +1,29 @@
{
"DynamicRules": [
{
"Name": "Process_Protect_Antivirus_Command",
"Name": "Self_Antivirus_Protect_Command",
"Code": 1001,
"Kill": false,
"Category": "Process",
"Initiator": [
"\\Device\\HarddiskVolume*\\Windows\\System32\\cmd.exe",
"\\Device\\HarddiskVolume*\\Windows\\SysWOW64\\cmd.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\WindowsPowerShell\\*\\powershell.exe",
"\\Device\\HarddiskVolume*\\Windows\\SysWOW64\\WindowsPowerShell\\*\\powershell.exe",
"\\Device\\HarddiskVolume*\\Program Files\\PowerShell\\*\\pwsh.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\taskkill.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\taskmgr.exe",
"\\Device\\HarddiskVolume*\\Program Files (x86)\\PowerShell\\*\\pwsh.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\wscript.exe",
"\\Device\\HarddiskVolume*\\Windows\\SysWOW64\\wscript.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\cscript.exe",
"\\Device\\HarddiskVolume*\\Windows\\SysWOW64\\cscript.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\mshta.exe",
"\\Device\\HarddiskVolume*\\Windows\\SysWOW64\\mshta.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\wsl.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\bash.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\rundll32.exe",
"\\Device\\HarddiskVolume*\\Windows\\SysWOW64\\rundll32.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\regsvr32.exe",
"\\Device\\HarddiskVolume*\\Windows\\SysWOW64\\regsvr32.exe",
],
"InitiatorExclude": [
"*\\PYAS.exe",
@@ -18,6 +31,14 @@
"CommandLine": [
"*taskkill*PYAS.exe*",
"*taskkill*msedgewebview2.exe*",
"*Stop-Process*PYAS*",
"*Stop-Process*msedgewebview2*",
"*Get-Process*PYAS*Stop-Process*",
"*Get-Process*msedgewebview2*Stop-Process*",
"*wmic*PYAS.exe*call terminate*",
"*wmic*PYAS.exe*delete*",
"*wmic*msedgewebview2.exe*call terminate*",
"*wmic*msedgewebview2.exe*delete*",
],
"Operations": [
"Execute",
@@ -28,27 +49,36 @@
"*\\powershell.exe",
"*\\pwsh.exe",
"*\\wmic.exe",
"*\\net.exe",
"*\\net1.exe",
],
"Priority": 110,
"FileOpenNameAvailable": true,
"SubsystemProcess": false,
},
{
"Name": "Process_Protect_PYAS_Handle",
"Name": "Self_Antivirus_Protect_Handle",
"Code": 1002,
"Kill": false,
"Category": "Process",
"InitiatorExclude": [
"*\\PYAS.exe",
"\\Device\\HarddiskVolume*\\Users\\*\\AppData\\Local\\Temp\\*\\PYAS_Setup.tmp",
"\\Device\\HarddiskVolume*\\Users\\*\\AppData\\Local\\Temp\\*\\_unins.tmp",
"\\Device\\HarddiskVolume*\\Users\\*\\AppData\\Local\\sogoupdf\\pdfpopcl.exe",
"\\Device\\HarddiskVolume*\\Users\\*\\AppData\\Local\\WeGame\\*\\tiny_dl\\TinyDL.exe",
"\\Device\\HarddiskVolume*\\Users\\*\\AppData\\Local\\Microsoft\\OneDrive\\OneDrive.exe",
"\\Device\\HarddiskVolume*\\Windows\\explorer.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\csrss.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\lsass.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\services.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\svchost.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\msiexec.exe",
"\\Device\\HarddiskVolume*\\Windows\\SystemApps\\*\\SearchHost.exe",
"\\Device\\HarddiskVolume*\\Program Files\\*",
"\\Device\\HarddiskVolume*\\Program Files (x86)\\*",
"*\\games\\Arknights Game\\Arknights.exe",
"*\\games\\EndField Game\\EndField.exe",
"*\\cm\\FeverGames\\*\\FeverGamesInstaller.exe",
"*\\Davinci\\Resolve.exe",
],
"Target": [
"*\\PYAS.exe",
@@ -81,17 +111,108 @@
]
},
{
"Name": "File_Protect_Antivirus",
"Name": "Attack_Command_Block_EFI_Mount",
"Code": 1003,
"Kill": false,
"Category": "Process",
"Target": [
"*\\mountvol.exe",
],
"CommandLine": [
"*mountvol* /S*",
],
"Operations": [
"Execute",
],
"Priority": 170,
"FileOpenNameAvailable": true,
},
{
"Name": "Attack_Command_Block_Destructive_Cmd_Delete",
"Code": 1004,
"Kill": true,
"Category": "Process",
"Target": [
"*\\cmd.exe",
],
"CommandLine": [
"*rd /s /q C:\\*",
"*rmdir /s /q C:\\*",
"*del X:\\* /s /f /q*",
"*rd /s /q X:\\EFI*",
],
"Operations": [
"Execute",
],
"Priority": 190,
"FileOpenNameAvailable": true,
},
{
"Name": "Attack_Command_Block_Forced_OOBE_Sysprep",
"Code": 1005,
"Kill": false,
"Category": "Process",
"Target": [
"*\\sysprep.exe",
],
"CommandLine": [
"*sysprep* /oobe*",
"*sysprep*/oobe*",
"* /oobe* /reboot*",
"* /generalize* /oobe*",
],
"Operations": [
"Execute",
],
"Priority": 185,
"FileOpenNameAvailable": true,
},
{
"Name": "Process_Protect_UserWritable_SystemName_Dll_Load",
"Code": 1006,
"Kill": false,
"Category": "Process",
"Initiator": [
"\\Device\\HarddiskVolume*\\Users\\*\\AppData\\*\\svchost.exe",
"\\Device\\HarddiskVolume*\\Users\\*\\AppData\\*\\rundll32.exe",
"\\Device\\HarddiskVolume*\\Users\\*\\AppData\\*\\regsvr32.exe",
"\\Device\\HarddiskVolume*\\Users\\*\\AppData\\*\\dllhost.exe",
"\\Device\\HarddiskVolume*\\Users\\*\\AppData\\*\\msiexec.exe",
"\\Device\\HarddiskVolume*\\Users\\*\\AppData\\*\\taskhostw.exe",
"\\Device\\HarddiskVolume*\\Users\\*\\AppData\\*\\spoolsv.exe",
"\\Device\\HarddiskVolume*\\Users\\*\\Downloads\\*\\svchost.exe",
"\\Device\\HarddiskVolume*\\Users\\*\\Downloads\\*\\rundll32.exe",
"\\Device\\HarddiskVolume*\\Users\\*\\Downloads\\*\\regsvr32.exe",
"\\Device\\HarddiskVolume*\\Users\\*\\Desktop\\*\\svchost.exe",
"\\Device\\HarddiskVolume*\\Users\\*\\Desktop\\*\\rundll32.exe",
"\\Device\\HarddiskVolume*\\Users\\*\\Desktop\\*\\regsvr32.exe",
"\\Device\\HarddiskVolume*\\ProgramData\\*\\svchost.exe",
"\\Device\\HarddiskVolume*\\ProgramData\\*\\rundll32.exe",
"\\Device\\HarddiskVolume*\\ProgramData\\*\\regsvr32.exe",
],
"Target": [
"\\Device\\HarddiskVolume*\\Users\\*\\AppData\\*\\*.dll",
"\\Device\\HarddiskVolume*\\Users\\*\\Downloads\\*\\*.dll",
"\\Device\\HarddiskVolume*\\Users\\*\\Desktop\\*\\*.dll",
"\\Device\\HarddiskVolume*\\ProgramData\\*\\*.dll",
],
"Operations": [
"ImageLoad",
],
"Priority": 160,
},
{
"Name": "Self_Antivirus_Protect_File",
"Code": 2001,
"Kill": false,
"Category": "File",
"InitiatorExclude": [
"*\\PYAS.exe",
"*\\PYAS_Setup.exe",
"*\\unins000.exe",
"*\\notepad*.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\cleanmgr.exe",
"\\Device\\HarddiskVolume*\\Users\\*\\AppData\\Local\\Temp\\*\\PYAS_Setup.tmp",
"\\Device\\HarddiskVolume*\\Users\\*\\AppData\\Local\\Temp\\*\\_unins.tmp",
"\\Device\\HarddiskVolume*\\Windows\\explorer.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\cleanmgr.exe",
],
"Target": [
"*\\PYAS.exe",
@@ -109,7 +230,7 @@
"*\\Interface\\static\\img\\*.ico",
"*\\Interface\\static\\js\\*.js",
"*\\Interface\\templates\\*.html",
"*\\Plugins\\Filter\\PYAS_Driver.sys",
"*\\Plugins\\Filter\\*.sys",
"*\\Plugins\\Rules\\*.json",
],
"Operations": [
@@ -118,20 +239,25 @@
]
},
{
"Name": "File_Protect_System",
"Name": "System_Boot_Protect_File",
"Code": 2002,
"Kill": true,
"Category": "File",
"InitiatorExclude": [
"*\\PYAS.exe",
"*\\Huorong\\Sysdiag\\bin\\sysclean.exe",
"*\\Dell\\SARemediation\\agent\\DellSupportAssistRemedationService.exe",
"\\Device\\HarddiskVolume*\\Windows\\explorer.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\SearchIndexer.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\lsass.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\svchost.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\services.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\taskhostw.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\TrustedInstaller.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\ReAgentc.exe",
"\\Device\\HarddiskVolume*\\Windows\\WinSxS\\*\\TiWorker.exe",
"\\Device\\HarddiskVolume*\\Windows\\UUS\\amd64\\wuaucltcore.exe",
"\\Device\\HarddiskVolume*\\$WinREAgent\\Scratch\\*\\DismHost.exe",
],
"Target": [
"\\Device\\HarddiskVolume*\\bootmgr",
@@ -141,15 +267,19 @@
"\\Device\\HarddiskVolume*\\Boot\\*",
"\\Device\\HarddiskVolume*\\Recovery\\*",
],
"TargetExclude": [
"\\Device\\HarddiskVolume*\\Windows\\System32\\Recovery\\ReAgent.xml",
"\\Device\\HarddiskVolume*\\Recovery\\OEM\\Logs\\Versions.ini",
],
"Operations": [
"Write",
"Delete",
]
},
{
"Name": "File_Protect_System_Exploit",
"Name": "Illegal_Characters_Protect_File",
"Code": 2003,
"Kill": true,
"Kill": false,
"Category": "File",
"InitiatorExclude": [
"*\\PYAS.exe",
@@ -179,17 +309,23 @@
]
},
{
"Name": "File_Protect_System_Ransom",
"Name": "System_Ransom_Protect_File",
"Code": 2004,
"Kill": true,
"Category": "File",
"Initiator": [
"\\Device\\HarddiskVolume*\\Windows\\System32\\cmd.exe",
"\\Device\\HarddiskVolume*\\Windows\\SysWOW64\\cmd.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\WindowsPowerShell\\*\\powershell.exe",
"\\Device\\HarddiskVolume*\\Windows\\SysWOW64\\WindowsPowerShell\\*\\powershell.exe",
"\\Device\\HarddiskVolume*\\Program Files\\PowerShell\\*\\pwsh.exe",
"\\Device\\HarddiskVolume*\\Program Files (x86)\\PowerShell\\*\\pwsh.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\wscript.exe",
"\\Device\\HarddiskVolume*\\Windows\\SysWOW64\\wscript.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\cscript.exe",
"\\Device\\HarddiskVolume*\\Windows\\SysWOW64\\cscript.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\mshta.exe",
"\\Device\\HarddiskVolume*\\Windows\\SysWOW64\\mshta.exe",
],
"Target": [
"\\Device\\HarddiskVolume*\\Windows\\*",
@@ -210,7 +346,7 @@
"TimeWindow": 3000,
},
{
"Name": "File_Protect_Ransom",
"Name": "User_Ransom_Protect_File",
"Code": 2005,
"Kill": true,
"Category": "File",
@@ -249,6 +385,8 @@
"*\\resources\\bin\\SDK\\DownloadSDKServer.exe",
"*\\bin\\*\\codex.exe",
"*\\bin\\codex.exe",
"*\\Weixin\\Weixin.exe",
"*\\Audacity\\Audacity.exe",
],
"Target": [
"\\Device\\HarddiskVolume*\\Users\\*",
@@ -265,9 +403,19 @@
"\\Device\\HarddiskVolume*\\Users\\*\\Temp\\*",
"\\Device\\HarddiskVolume*\\Users\\*\\Local\\Temp\\*",
"\\Device\\HarddiskVolume*\\Users\\*\\cache\\*",
"\\Device\\HarddiskVolume*\\Users\\*\\.cache\\*",
"\\Device\\HarddiskVolume*\\Users\\*\\.aws\\*",
"\\Device\\HarddiskVolume*\\Users\\*\\.codex\\*",
"\\Device\\HarddiskVolume*\\Users\\*\\.designer\\*",
"\\Device\\HarddiskVolume*\\Users\\*\\.idlerc\\*",
"\\Device\\HarddiskVolume*\\Users\\*\\.keras\\*",
"\\Device\\HarddiskVolume*\\Users\\*\\.matplotlib\\*",
"\\Device\\HarddiskVolume*\\Users\\*\\.ssh\\*",
"*\\ventoy\\Boot\\*",
"*\\AdminRegionConfig.*",
"*\\nt_qq\\nt_*\\*",
"*\\nt_qq\\global\\nt_*\\*",
"\\Device\\HarddiskVolume*\\Users\\*\\Documents\\Tencent Files\\*",
],
"Extensions": [
".zip",
@@ -300,22 +448,17 @@
".mov",
".wmv",
".mkv",
".apk",
".jar",
".class",
".db",
".sqlite",
],
"Operations": [
"Write",
"Delete",
"Rename",
],
"Threshold": 5,
"Threshold": 10,
"TimeWindow": 3000,
},
{
"Name": "Device_Protect_Disk_Wiper",
"Name": "System_Boot_Protect_Disk_Wiper",
"Code": 2006,
"Kill": false,
"Category": "Device",
@@ -323,20 +466,22 @@
"*\\PYAS.exe",
"*\\rufus-*.exe",
"*\\DiskInfo*.exe",
"\\Device\\HarddiskVolume*\\Program Files\\*\\launcher\\ca_launcher.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\diskpart.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\diskmgmt.msc",
"\\Device\\HarddiskVolume*\\Windows\\System32\\taskhostw.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\vds.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\format.com",
"\\Device\\HarddiskVolume*\\Windows\\System32\\WinSAT.exe",
],
"Operations": [
"Ioctl",
]
},
{
"Name": "Registry_Protect_System",
"Name": "Registry_Protect_System_Suspicious",
"Code": 3001,
"Kill": true,
"Kill": false,
"Category": "Registry",
"InitiatorExclude": [
"\\Device\\HarddiskVolume*\\Windows\\*",
@@ -344,55 +489,109 @@
"\\Device\\HarddiskVolume*\\Program Files (x86)\\*",
"\\Device\\HarddiskVolume*\\ProgramData\\Microsoft\\Windows Defender\\*",
"\\Device\\HarddiskVolume*\\ProgramData\\Microsoft\\Windows Defender Advanced Threat Protection\\*",
"*\\Google\\Chrome\\Application\\chrome.exe",
"*\\Microsoft\\Edge\\Application\\msedge.exe",
"*\\Mozilla Firefox\\firefox.exe",
"*\\Thorium\\Application\\thorium.exe",
"*\\Sources\\SetupHost.exe",
"*\\Work\\*\\DismHost.exe",
"*\\Steam\\steam.exe",
"*\\steam\\*\\steam.exe",
"*\\Steam\\steamapps\\common\\*",
"*\\Battle.net\\*\\Battle.net.exe",
"*\\Epic Games\\Launcher\\Portal\\Binaries\\*\\EpicGamesLauncher.exe",
"*\\PC4399_WPFLauncher.exe",
"*\\Neverness To Everness\\NTELauncher.exe",
"*\\ThunderEv.exe",
"*\\ThunderStart.exe",
"*\\bin\\APlayer.exe",
"*\\bilibili\\bilibili.exe",
"*\\KuGou.exe",
"*\\XLGame.exe",
"*\\XLLiveUD.exe",
"*\\hrkill-*.exe",
"*\\Huorong\\Sysdiag\\bin\\HipsDaemon.exe",
"*\\360Safe.exe",
"*\\PYAS.exe",
"*\\WPS Office\\ksolaunch.exe",
"*\\WPS Office\\*\\wps.exe",
"*\\WPS Office\\*\\*.exe",
"*\\Application\\AndrowsLauncher.exe",
"*\\resources\\bin\\SDK\\DownloadSDKServer.exe",
"*\\bin\\*\\codex.exe",
"*\\bin\\codex.exe",
],
"Target": [
"\\REGISTRY\\MACHINE\\BCD00000000",
"\\REGISTRY\\MACHINE\\BCD00000000\\*",
"*\\Classes\\.exe",
"*\\Classes\\.bat",
"*\\Classes\\.cmd",
"*\\Classes\\.com",
"*\\Classes\\exefile\\*",
"*\\Classes\\batfile\\*",
"*\\Classes\\cmdfile\\*",
"*\\Classes\\comfile\\*",
"*\\mouclass",
"*\\kbdclass",
"*\\DisableAntiSpyware",
"*\\DisableWindowsUpdateAccess",
"*\\NoWindowsUpdate",
"*\\AutoAdminLogon",
"*\\DefaultUserName",
"*\\DefaultPassword",
"*\\AltDefaultUserName",
"*\\AltDefaultPassword",
"*\\AutoReboot",
"*\\DisableAutomaticRestartOnFailure",
"*\\SystemBiosVersion",
"*\\ClearRecentDocsOnExit",
"*\\HideClock",
"*\\HideFastUserSwitching",
"*\\ShutdownWithoutLogon",
"*\\DisableCAD",
],
"Operations": [
"Write",
"Delete",
"Create",
],
"Priority": 130,
},
{
"Name": "Registry_Protect_Open_With_Suspicious",
"Code": 3002,
"Kill": false,
"Category": "Registry",
"InitiatorExclude": [
"\\Device\\HarddiskVolume*\\Windows\\*",
"\\Device\\HarddiskVolume*\\Program Files\\*",
"\\Device\\HarddiskVolume*\\Program Files (x86)\\*",
"*\\PYAS.exe",
],
"Target": [
"*\\Classes\\.exe",
"*\\Classes\\.com",
"*\\Classes\\.bat",
"*\\Classes\\.cmd",
"*\\Classes\\exefile\\*",
"*\\Classes\\comfile\\*",
"*\\Classes\\batfile\\*",
"*\\Classes\\cmdfile\\*",
],
"Operations": [
"Write",
"Delete",
"Create",
],
"Priority": 150,
},
{
"Name": "Registry_Protect_BCD_Suspicious",
"Code": 3003,
"Kill": false,
"Category": "Registry",
"InitiatorExclude": [
"\\Device\\HarddiskVolume*\\Windows\\*",
"\\Device\\HarddiskVolume*\\ProgramData\\Microsoft\\Windows Defender\\*",
"\\Device\\HarddiskVolume*\\ProgramData\\Microsoft\\Windows Defender Advanced Threat Protection\\*",
"*\\PYAS.exe",
],
"Target": [
"\\REGISTRY\\MACHINE\\BCD00000000",
"\\REGISTRY\\MACHINE\\BCD00000000\\*",
],
"Operations": [
"Write",
"Delete",
"Create",
],
"Priority": 140,
},
{
"Name": "Registry_Protect_System_Critical",
"Code": 3004,
"Kill": true,
"Category": "Registry",
"InitiatorExclude": [
"\\Device\\HarddiskVolume*\\Windows\\*",
"*\\PYAS.exe",
],
"Target": [
"*\\EnableLUA",
"*\\ConsentPromptBehaviorAdmin",
"*\\ConsentPromptBehaviorUser",
"*\\EnableSecureUIAPaths",
"*\\BootExecute",
"*\\SubSystems",
"*\\FirmwareBootDevice",
"*\\BootDriverFlags",
"*\\SystemStartOptions",
"*\\Userinit",
"*\\UIHost",
"*\\NoControlPanel",
"*\\NoDrives",
"*\\NoFileMenu",
@@ -407,7 +606,6 @@
"*\\NoFolderOptions",
"*\\RestrictRun",
"*\\NoViewContextMenu",
"*\\HideClock",
"*\\NoStartMenuMyGames",
"*\\NoStartMenuMyMusic",
"*\\DisableCMD",
@@ -426,7 +624,6 @@
"*\\NoActiveDesktop",
"*\\NoSetActiveDesktop",
"*\\NoRecentDocsMenu",
"*\\NoWindowsUpdate",
"*\\NoChangeStartMenu",
"*\\NoFavoritesMenu",
"*\\NoRecentDocsHistory",
@@ -436,44 +633,95 @@
"*\\NoManageMyComputerVerb",
"*\\NoRealMode",
"*\\NoRun",
"*\\ClearRecentDocsOnExit",
"*\\NoActiveDesktopChanges",
"*\\NoStartMenuNetworkPlaces",
"*\\NoLockScreen",
"*\\HideFastUserSwitching",
"*\\ConsentPromptBehaviorUser",
"*\\EnableSecureUIAPaths",
"*\\NoSRM",
"*\\NoInteractiveServices",
"*\\ShutdownWithoutLogon",
"*\\AutoAdminLogon",
"*\\BootExecute",
"*\\SubSystems",
"*\\FirmwareBootDevice",
"*\\BootDriverFlags",
"*\\SystemStartOptions",
"*\\DisableAutomaticRestartOnFailure",
"*\\SystemBiosVersion",
"*\\NoTheme",
"*\\SystemSetupInProgress",
"*\\SetupType",
"*\\AutoReboot",
"*\\Userinit",
"*\\UIHost",
"*\\Debugger",
"*\\DefaultUserName",
"*\\DefaultPassword",
"*\\AltDefaultUserName",
"*\\AltDefaultPassword",
"*\\DisableCAD",
"*\\LegalNoticeCaption",
"*\\LegalNoticeText",
],
"Operations": [
"Write",
"Delete",
"Create",
]
}
],
"Priority": 180,
},
{
"Name": "Registry_Protect_SafeMode_Autostart",
"Code": 3005,
"Kill": true,
"Category": "Registry",
"InitiatorExclude": [
"\\Device\\HarddiskVolume*\\Windows\\*",
"\\Device\\HarddiskVolume*\\ProgramData\\Microsoft\\Windows Defender\\*",
"\\Device\\HarddiskVolume*\\ProgramData\\Microsoft\\Windows Defender Advanced Threat Protection\\*",
"*\\Sources\\SetupHost.exe",
"*\\Work\\*\\DismHost.exe",
"*\\Huorong\\Sysdiag\\bin\\HipsDaemon.exe",
"*\\Huorong\\Sysdiag\\bin\\HRUpdate.exe",
"*\\360Safe.exe",
"*\\PYAS.exe",
],
"Target": [
"\\REGISTRY\\MACHINE\\SYSTEM\\*ControlSet*\\Control\\SafeBoot\\Minimal\\*",
"\\REGISTRY\\MACHINE\\SYSTEM\\*ControlSet*\\Control\\SafeBoot\\Network\\*",
"\\REGISTRY\\MACHINE\\SYSTEM\\*ControlSet*\\Control\\SafeBoot\\AlternateShell",
"\\REGISTRY\\MACHINE\\SYSTEM\\*ControlSet*\\Control\\SafeBoot\\AlternateShell\\*",
"\\REGISTRY\\MACHINE\\SYSTEM\\*ControlSet*\\Control\\SafeBoot\\Option",
"\\REGISTRY\\MACHINE\\SYSTEM\\*ControlSet*\\Control\\SafeBoot\\Option\\*",
],
"TargetExclude": [
"\\REGISTRY\\MACHINE\\SYSTEM\\ControlSet001\\Control\\SafeBoot\\Network\\HipsDaemon",
],
"Operations": [
"Write",
"Delete",
"Create",
],
"Priority": 170,
},
{
"Name": "Registry_Protect_OOBE_Reset",
"Code": 3006,
"Kill": false,
"Category": "Registry",
"InitiatorExclude": [
"*\\PYAS.exe",
"*\\Sources\\SetupHost.exe",
"*\\Work\\*\\DismHost.exe",
"\\Device\\HarddiskVolume*\\Windows\\UUS\\amd64\\MoUsoCoreWorker.exe",
"\\Device\\HarddiskVolume*\\Windows\\UUS\\Packages\\Preview\\amd64\\MoUsoCoreWorker.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\MoUsoCoreWorker.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\Sysprep\\sysprep.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\oobe\\*",
"\\Device\\HarddiskVolume*\\Windows\\System32\\svchost.exe",
"\\Device\\HarddiskVolume*\\Windows\\System32\\taskhostw.exe",
"\\Device\\HarddiskVolume*\\Windows\\explorer.exe",
],
"Target": [
"\\REGISTRY\\MACHINE\\SYSTEM\\Setup\\OOBEInProgress",
"\\REGISTRY\\MACHINE\\SYSTEM\\Setup\\SystemSetupInProgress",
"\\REGISTRY\\MACHINE\\SYSTEM\\Setup\\SetupType",
"\\REGISTRY\\MACHINE\\SYSTEM\\Setup\\CmdLine",
"\\REGISTRY\\MACHINE\\SYSTEM\\Setup\\AuditInProgress",
"\\REGISTRY\\MACHINE\\SYSTEM\\Setup\\MiniSetupInProgress",
"\\REGISTRY\\MACHINE\\SYSTEM\\Setup\\SetupPhase",
"\\REGISTRY\\MACHINE\\SYSTEM\\Setup\\Status\\SysprepStatus\\*",
"\\REGISTRY\\MACHINE\\SYSTEM\\Setup\\Status\\ChildCompletion\\*",
"\\REGISTRY\\MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Setup\\State\\ImageState",
"\\REGISTRY\\MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Setup\\OOBE\\*",
"\\REGISTRY\\MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\OOBE\\*",
],
"TargetExclude": [
"\\REGISTRY\\MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\OOBE\\TelemetryCorrelation",
],
"Operations": [
"Write",
"Delete",
"Create",
],
"Priority": 175,
},
]
}
}
+334 -132
View File
@@ -1,164 +1,366 @@
# PYAS Security
Antivirus software written in Python and C++ that blocks threats through Machine Learning and behavioral monitoring!
<div align="center">
<img width="2440" height="1600" alt="PYAS_All" src="https://github.com/user-attachments/assets/1991aad3-64cc-4266-ac67-dab70c891ce7" />
**A hybrid Windows endpoint security platform powered by machine learning, YARA rules, cloud analysis, and kernel-level behavioral protection.**
## Requirements
[![Latest Release](https://img.shields.io/github/v/release/87owo/PYAS?display_name=tag&style=flat-square)](https://github.com/87owo/PYAS/releases/latest)
[![GitHub Stars](https://img.shields.io/github/stars/87owo/PYAS?style=flat-square)](https://github.com/87owo/PYAS/stargazers)
[![GitHub Forks](https://img.shields.io/github/forks/87owo/PYAS?style=flat-square)](https://github.com/87owo/PYAS/network/members)
[![Platform](https://img.shields.io/badge/platform-Windows%2010%2F11-0078D4?style=flat-square&logo=windows)](https://github.com/87owo/PYAS)
[![Python](https://img.shields.io/badge/Python-3.10-3776AB?style=flat-square&logo=python&logoColor=white)](https://www.python.org/)
[![C++](https://img.shields.io/badge/kernel-C%2B%2B-00599C?style=flat-square&logo=cplusplus)](https://github.com/87owo/PYAS/tree/main/Plugins/Filter)
Python 3.10 is recommended. Other Python versions may require different pip commands.
[Website](https://pyas-security.com/antivirus) · [Online Analysis](https://pyas-security.com/analyze) · [Download](https://github.com/87owo/PYAS/releases) · [Report an Issue](https://github.com/87owo/PYAS/issues)
```
pip install pystray
pip install pefile
pip install requests
pip install pywebview
pip install Pillow
pip install yara-python
pip install numpy
pip install onnxruntime
```
</div>
Non-essential requirements installation, only used for model training or other functions.
![PYAS Security interface](https://github.com/user-attachments/assets/4a7e6b52-7001-4726-96fe-a2d6ccc8e6a4)
```
pip install pandas
pip install scikit-learn
pip install lightgbm
pip install onnxmltools
pip install orjson
```
## Overview
## File Information
PYAS Security is a source-available Windows endpoint security project that combines multiple detection and protection layers in one desktop application. It brings together local PE-file machine-learning inference, YARA-based matching, digital-signature inspection, optional cloud analysis, real-time user-mode monitoring, and a native Windows minifilter driver.
The following lists the storage locations of all relevant code and other related documents.
The project is designed both as a usable security application and as an engineering platform for studying malware detection, Windows internals, rule-driven prevention, model training, and online file analysis.
```
PYAS/
├── Engine/ # Complete code for Yara signatures and AI model training
│ ├── Heuristic/
│ │ └── ... # Yara Rules
│ └── Properties/
│ └── ... # LightGBM Model
│
├── Interface/ # Interface Interaction and Icons with WebView2
│ ├── static/
│ │ └── ... # style.css, main.js, icon.ico
│ └── templates/
│ └── ... # index.html
│
├── Plugins/ # Main filter driver protect and rules
│ ├── Filter/
│ │ └── ... # DriverCommon.h, DriverEntry.cpp, ProtectRegistry.cpp, ...
│ └── Rules/
│ └── ... # Rules_Driver_P1.json
|
├── PYAS.py # Main antivirus application entry point
├── PYAS_Engine.py # Core scanning engine algorithm
├── PYAS_Version.py # Version metadata for packaging and updates
└── ...
```
> [!IMPORTANT]
> PYAS is an independent security project. No antivirus product can guarantee detection of every threat. Evaluate it in a controlled environment before production use, keep backups, and review alerts before deleting files.
## Architecture diagram
## Why PYAS
PYAS Security antivirus software general architecture diagram.
- **Layered detection** — combines YARA, PE feature analysis, ONNX models, signature verification, and cloud-assisted analysis.
- **Behavior-oriented protection** — monitors processes, files, memory, network activity, system settings, and sensitive operations.
- **Kernel integration** — includes a C++ minifilter driver with configurable protection rules and user-mode communication.
- **Local-first scanning** — performs core static analysis on the endpoint; cloud submission is separately configurable.
- **Operational tooling** — adds quarantine, allow/block lists, system repair, startup management, cleanup, and activity reporting.
- **Research-ready codebase** — includes model-training utilities, an online analysis service, an API client, experimental engines, and a visual rule editor.
## Protection capabilities
| Layer | Capability | Implementation |
|---|---|---|
| Static rules | File and process-memory matching | YARA rules compiled and loaded by `rule_scanner` |
| PE machine learning | Feature extraction and local classification | `pefile`, NumPy, ONNX Runtime |
| Signature trust | Authenticode verification | Windows WinVerifyTrust APIs |
| Cloud analysis | Upload, polling, rescan, and result retrieval | HTTPS API with chunked upload support |
| Process protection | New-process inspection, optional suspension, termination controls | Win32/NT APIs and worker pools |
| File protection | Directory change monitoring, debounced real-time scans, file locking | Windows file notification APIs |
| Memory protection | Process-memory YARA scanning and kernel callbacks | User-mode scanner plus driver rules |
| Network visibility | Process-aware TCP connection monitoring | Windows IP Helper APIs |
| Kernel enforcement | File, process/thread, registry, memory, image-load, and boot/disk controls | Native C++ minifilter driver |
| Recovery and maintenance | Quarantine, system repair, startup management, cleanup, MBR backup/check | Python application services |
Protection modules are individually configurable. Several active-protection switches are disabled by default so users can enable only the controls appropriate for their environment.
## Detection pipeline
```mermaid
graph TD
%% Global Styles
classDef userMode fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#0d47a1
classDef kernelMode fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#e65100
classDef storage fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:#4a148c
classDef interaction fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,stroke-dasharray: 5 5,color:#1b5e20
subgraph UserMode ["User Mode (Python / Web)"]
UI["Web UI (pywebview)"]:::userMode
PYASCore["PYAS Core (PYAS.py)"]:::userMode
subgraph Engines ["Scanning Engines (PYAS_Engine.py)"]
PEScanner["PE/ML Scanner (pe_scanner)"]:::userMode
RuleScanner["YARA Scanner (rule_scanner)"]:::userMode
CloudScanner["Cloud API (cloud_scanner)"]:::userMode
SignScanner["Signature Verify (sign_scanner)"]:::userMode
end
CommClient["Filter Communication Client"]:::interaction
end
subgraph KernelMode ["Kernel Mode (C++ Minifilter Driver)"]
CommServer["ALPC Port (\\PYAS_Output_Pipe)"]:::interaction
DriverCore["Driver Entry (DriverEntry.cpp)"]:::kernelMode
RuleEngine["Rules & Trust Cache (ProtectRules.cpp)"]:::kernelMode
subgraph Protections ["Protection Subsystems"]
FileProtect["File Minifilter (ProtectFile.cpp)"]:::kernelMode
ProcessProtect["ObRegisterCallbacks (ProtectProcess.cpp)"]:::kernelMode
RegProtect["CmRegisterCallbackEx (ProtectRegistry.cpp)"]:::kernelMode
BootProtect["IRP_MJ_DEVICE_CONTROL (ProtectBoot.cpp)"]:::kernelMode
end
end
subgraph OS ["Windows OS & Storage"]
FS["File System (FltMgr)"]:::storage
Reg["Registry (CmMgr)"]:::storage
ProcMgr["Process/Thread Manager (ObMgr)"]:::storage
Disk["Physical Disk / MBR"]:::storage
end
CloudAPI["PYAS Cloud Server"]:::storage
%% User Mode Internal
UI <-->|JS Evaluate / JSON| PYASCore
PYASCore -->|Extract Features & Scan| Engines
CloudScanner -.->|HTTPS REST API| CloudAPI
PYASCore <-->|FilterSendMessage / FilterGetMessage| CommClient
%% User-Kernel Boundary
CommClient <-->|PYAS_MESSAGE / PYAS_USER_MESSAGE| CommServer
%% Kernel Mode Internal
CommServer <-->|Message Dispatch| DriverCore
DriverCore --> RuleEngine
RuleEngine --> Protections
%% Kernel-OS Boundary
FileProtect -->|IRP_MJ_CREATE, IRP_MJ_WRITE...| FS
ProcessProtect -->|PreOpenProcess, PreOpenThread| ProcMgr
RegProtect -->|RegNtPreCreateKey, RegNtPreSetValueKey...| Reg
BootProtect -->|IOCTL_DISK_FORMAT_TRACKS...| Disk
flowchart LR
A[File or process event] --> B{Scope and policy checks}
B -->|Allowed| C[Hash and metadata]
B -->|Excluded| Z[Skip]
C --> D[Digital signature verification]
C --> E[YARA rule scan]
C --> F[PE feature extraction]
F --> G[ONNX model inference]
D --> H[Local verdict aggregation]
E --> H
G --> H
H --> I{Cloud analysis enabled?}
I -->|Yes| J[Hash lookup / chunked upload]
J --> K[Cloud report]
I -->|No| L[Local result]
K --> M[Alert and response]
L --> M
M --> N[Quarantine / delete / allow]
```
## Machine Learning
## System architecture
The machine learning-based virus scanning engine training achieved extremely high accuracy.
```mermaid
flowchart TB
subgraph UI[Presentation layer]
WEB[HTML / CSS / JavaScript]
WV[WebView2 desktop shell]
TRAY[System tray and Windows shell integration]
end
<img width="895" height="1097" alt="train" src="https://github.com/user-attachments/assets/fa7cf0e2-01a1-4234-9358-718c2b8a2c9a" />
subgraph APP[Python application layer]
CORE[PYAS.py orchestration]
SCAN[ScannerMixin]
PROTECT[ProtectMixin]
TOOLS[ToolsMixin]
ENGINE[PYAS_Engine.py]
end
## Support System
subgraph DETECT[Detection engines]
YARA[YARA rules]
PE[PE feature extractor]
ONNX[ONNX Runtime models]
SIGN[Authenticode verification]
CLOUD[PYAS Cloud API]
end
Please ensure your computer supports Microsoft Visual C++ 2015-2022 Redistributable and Edge Webview2.
subgraph KERNEL[Windows kernel layer]
PORT[Filter Manager communication port]
RULES[Dynamic rule engine and trust cache]
FILE[File-system minifilter]
PROC[Process and thread callbacks]
REG[Registry callbacks]
MEM[Memory and image-load controls]
BOOT[Boot and disk I/O controls]
end
| Config | Permissions | System version | Processor | Memory | Storage |
|-----------|---------------|----------------------|-----------|--------|---------|
| Minimum | Administrator | >= Windows 10 (20H1) | 1 GHz | 300MB | 100MB |
| Recommend | Administrator | >= Windows 10 (21H2) | 3 GHz | 500MB | 200MB |
subgraph ONLINE[Online analysis platform]
API[Flask application and REST API]
QUEUE[Priority analysis workers]
DB[(PostgreSQL)]
STORAGE[(Sample and report storage)]
end
## Official Website
WEB <--> WV
WV <--> CORE
TRAY <--> CORE
CORE --> SCAN
CORE --> PROTECT
CORE --> TOOLS
SCAN --> ENGINE
PROTECT --> ENGINE
ENGINE --> YARA
ENGINE --> PE
PE --> ONNX
ENGINE --> SIGN
ENGINE -. optional HTTPS .-> CLOUD
PROTECT <--> PORT
PORT <--> RULES
RULES --> FILE
RULES --> PROC
RULES --> REG
RULES --> MEM
RULES --> BOOT
CLOUD --> API
API --> QUEUE
QUEUE --> DB
QUEUE --> STORAGE
```
If you are interested in this project, you can visit the website to see other related content.
## Repository layout
Source Available : https://github.com/87owo/PYAS
```text
PYAS/
├── PYAS.py # Desktop entry point, UI bridge, configuration and lifecycle
├── PYAS_Engine.py # YARA, PE/ML, signature and cloud scanning engines
├── PYAS_Scanner.py # Scan scheduling, workers, results and remediation
├── PYAS_Protect.py # Real-time protection and driver lifecycle/communication
├── PYAS_Tools.py # Windows utilities, startup, process, network and cleanup tools
├── PYAS_Version.py # Windows executable version metadata
├── Engine/
│ ├── Heuristic/ # YARA signatures and rule assets
│ └── Properties/ # PE feature models and training/inference utilities
├── Interface/ # WebView2 HTML, CSS, JavaScript and icons
└── Plugins/
├── Filter/ # Native Windows minifilter driver source
└── Rules/ # Driver protection policies
```
Official Website : https://pyas-security.com/antivirus
## Getting started
Online Analyze : https://pyas-security.com/analyze
### Option 1 — Install a packaged release
Packaged Download: https://github.com/87owo/PYAS/releases
For most users, download the latest installer from [GitHub Releases](https://github.com/87owo/PYAS/releases).
## Project License
1. Confirm that the installer comes from the official `87owo/PYAS` repository.
2. Ensure Microsoft Visual C++ 2015–2022 Redistributable and Microsoft Edge WebView2 Runtime are installed.
3. Run the installer as an administrator.
4. Open PYAS and review the protection switches before enabling kernel-level controls.
For any questions, needs, or bug feedback, please contact us through the following website.
The current source tree may be ahead of the latest packaged release. Consult the version shown in the application and the release notes when reporting issues.
Source Issues : https://github.com/87owo/PYAS/issues
### Option 2 — Run from source
Official Email : service@pyas-security.com
Python 3.10 is the recommended development runtime.
```powershell
git clone https://github.com/87owo/PYAS.git
cd PYAS
py -3.10 -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
pip install pystray pefile requests pywebview Pillow yara-python numpy onnxruntime
python PYAS.py
```
Administrator privileges are required for features that interact with protected processes, system configuration, services, physical disks, or the kernel driver. Running the desktop UI without elevation may leave those capabilities unavailable.
### Optional training dependencies
The following packages are used by model-training or data-preparation utilities and are not required for the normal desktop runtime:
```powershell
pip install pandas scikit-learn lightgbm onnxmltools orjson
```
## Runtime requirements
| Profile | Operating system | Privileges | CPU | Memory | Free storage |
|---|---|---:|---:|---:|---:|
| Minimum | Windows 10 20H1 or newer | Administrator for full protection | 1 GHz | 300 MB | 100 MB |
| Recommended | Windows 10 21H2 / Windows 11 | Administrator | 3 GHz | 500 MB+ | 200 MB+ |
Required platform components:
- Microsoft Visual C++ 2015–2022 Redistributable
- Microsoft Edge WebView2 Runtime
- 64-bit Windows for the included x64 driver build
Actual resource usage depends on the selected scan scope, file sizes, enabled models, number of active workers, and real-time protection settings.
## Command-line integration
The application supports Windows shell and maintenance workflows through command-line arguments:
```text
PYAS.exe -scan <path> Scan a file or directory, forwarding to an existing instance
PYAS.exe -hide Start with the main window hidden
PYAS.exe -quit Request the running instance to exit
PYAS.exe -driver-unload Unload the active filter driver
PYAS.exe -driver-uninstall Stop and remove the driver service
```
These commands are primarily intended for installer, shell-menu, and maintenance integration. Administrative rights may be required.
## Configuration and local data
PYAS stores machine-wide configuration and reports under:
```text
%ProgramData%\PYAS\Config.json
%ProgramData%\PYAS\Report.json
```
WebView2 user data and its diagnostic log are stored under the current user's local application-data directory. Configuration includes scan limits, enabled protection layers, language and theme preferences, custom rule references, allow/block lists, and quarantine metadata.
Before reporting a configuration issue, reproduce it with default settings when safe to do so and remove sensitive paths or file information from logs.
## Online analysis service
The `Analyze/` component is a separate Flask and PostgreSQL application that provides:
- regular and chunked file uploads;
- asynchronous priority-based analysis tasks;
- SHA-256 report lookup and rescanning;
- structured PE metadata and similarity analysis;
- authenticated API access;
- search, statistics, comments, votes, and report export;
- encrypted sample packaging and controlled download workflows.
The service can be containerized with Docker Compose, but the checked-in deployment configuration must be treated as a development example. Replace all credentials and tokens with environment-managed secrets before deployment, restrict sample access, terminate TLS at a trusted reverse proxy, and isolate the analysis workers from production networks.
## Python API example
`PYAS_Cloud.py` provides a client for the online analysis API:
```python
from PYAS_Cloud import PYAS_Client
client = PYAS_Client(
api_key="YOUR_API_KEY",
hosts="https://pyas-security.com",
)
sha256 = client.upload_file(r"C:\samples\candidate.exe")
if sha256 and client.wait_for_analysis(sha256):
report = client.get_report(sha256)
print(report)
```
Only submit files that you are authorized to upload. Samples may contain confidential or personal information.
## Building the driver
The driver source is located in `Plugins/Filter/` and includes a Visual Studio solution and project.
Typical prerequisites:
- Visual Studio with Desktop development with C++;
- a compatible Windows Driver Kit (WDK);
- an x64 build environment;
- an appropriate test-signing or production code-signing workflow.
Build and test kernel components only in an isolated virtual machine with snapshots. Windows driver loading policies apply, and production distribution requires appropriate signing. Do not disable platform security controls on a primary workstation merely to load a development build.
## Rule editor
PYAS includes a Blockly-based visual editor for creating driver protection rules. The packaged editor is distributed through project releases.
![PYAS rule editor](https://github.com/user-attachments/assets/29a816a8-3bf9-4e1c-b881-7336676ad7f9)
Driver rules support matching across categories such as file, process, thread, registry, memory, device, and image-load activity, with allow/deny behavior, priority, operation masks, path relationships, thresholds, and other contextual constraints.
## Machine-learning engine
The local PE engine extracts structural and statistical characteristics from Windows executables, including headers, sections, data directories, imports, exports, resources, strings, byte distributions, entropy windows, overlay data, Rich headers, load configuration, certificates, and entry-point anomalies. Features are ordered according to the model metadata and evaluated through ONNX Runtime.
![Model training result](https://github.com/user-attachments/assets/fa7cf0e2-01a1-4234-9358-718c2b8a2c9a)
Model performance shown by training output should not be interpreted as a universal real-world detection rate. Reproducible evaluation requires a documented dataset split, class balance, deduplication strategy, temporal holdout, false-positive analysis, and testing against previously unseen malware families.
## Development and verification
Run the Windows-focused unit tests with:
```powershell
python -m unittest discover -s tests -v
```
Before submitting a change:
1. Keep modifications scoped to the affected protection layer.
2. Test both successful and denied Windows API paths.
3. Verify cleanup of handles, services, threads, temporary files, and driver ports.
4. Exercise the UI flow in both English and Traditional Chinese.
5. Test driver changes in a disposable VM, including unload and recovery behavior.
6. Record the Windows build, Python version, driver-signing mode, and enabled switches.
## Security and privacy
- Cloud scanning is optional; review the active configuration before analyzing confidential files.
- Kernel drivers and remediation features can affect system stability and data availability.
- Quarantine, deletion, startup changes, system repair, and MBR operations should be tested with recoverable data.
- Never deploy the example online-analysis stack with repository-default credentials or tokens.
- Treat uploaded malware samples as hostile. Use isolated storage, least privilege, network segmentation, and strict download authorization.
- If you discover a vulnerability, avoid publishing exploit details before the maintainer has had a reasonable opportunity to respond.
## Project status
PYAS is actively developed. The source tree can contain features intended for a future release, experimental modules, generated artifacts, and research data that are not part of the stable desktop package. For end-user installation, prefer signed artifacts from [Releases](https://github.com/87owo/PYAS/releases); for development, pin dependencies and validate the exact revision you build.
## Contributing
Issues and focused pull requests are welcome.
- Search [existing issues](https://github.com/87owo/PYAS/issues) before opening a new report.
- Include reproducible steps, expected and actual behavior, logs with sensitive data removed, and environment details.
- Keep unrelated refactors out of bug-fix pull requests.
- Do not commit malware samples, credentials, tokens, private certificates, or machine-specific build output.
## License and third-party components
The repository contains component-specific license files and bundled third-party assets. GitHub does not currently identify a repository-wide SPDX license. Review the applicable license files and obtain clarification from the maintainer before redistribution, commercial use, or incorporation into another product.
## Links and contact
- Source: [github.com/87owo/PYAS](https://github.com/87owo/PYAS)
- Releases: [github.com/87owo/PYAS/releases](https://github.com/87owo/PYAS/releases)
- Official website: [pyas-security.com/antivirus](https://pyas-security.com/antivirus)
- Online analysis: [pyas-security.com/analyze](https://pyas-security.com/analyze)
- Issue tracker: [github.com/87owo/PYAS/issues](https://github.com/87owo/PYAS/issues)
- Email: [[email protected]](mailto:[email protected])
---
<div align="center">
Built for Windows security research, layered endpoint defense, and transparent experimentation.
</div>