Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c1112b517 | ||
|
|
8c2085d851 | ||
|
|
5b9fd96dfe | ||
|
|
9a69acece8 | ||
|
|
4367226be8 | ||
|
|
be2088f6f3 | ||
|
|
9de779ba3f | ||
|
|
6cb8b801c7 | ||
|
|
b89e91cdb8 | ||
|
|
50b8378e0a | ||
|
|
dd108b5c8a | ||
|
|
be1d86ca66 | ||
|
|
2ef0fe145f | ||
|
|
af3a92c7d2 | ||
|
|
411e6dba8f | ||
|
|
9db8d33df4 | ||
|
|
814baadbdf | ||
|
|
bc21628387 | ||
|
|
e7634c3c37 | ||
|
|
05e952c5ee | ||
|
|
8a0209a99a | ||
|
|
65bc5c7c2a | ||
|
|
07c64042be | ||
|
|
22c04063cf | ||
|
|
68a7ab657a | ||
|
|
630dc67058 | ||
|
|
591507512c | ||
|
|
ff49cafc74 | ||
|
|
b077ba06bd | ||
|
|
a2b66aec64 | ||
|
|
4b692b0c87 | ||
|
|
99d1b20d0f | ||
|
|
fae5edb57b | ||
|
|
6db91483a0 | ||
|
|
90383f3d09 | ||
|
|
966a5a3592 | ||
|
|
48e14cd143 | ||
|
|
4264f50ba3 | ||
|
|
887e0718d0 | ||
|
|
4870a5c480 | ||
|
|
e8cadf0537 | ||
|
|
f5124e0ef2 | ||
|
|
da9a520632 | ||
|
|
81bebeda63 | ||
|
|
4df1432be1 | ||
|
|
e699be6b2e | ||
|
|
e8dda40e7f | ||
|
|
5b4cd6e879 | ||
|
|
316b061d34 | ||
|
|
6ff27679e7 | ||
|
|
8f3adbc870 | ||
|
|
5798a35456 | ||
|
|
a8a2060978 | ||
|
|
7cf2763511 | ||
|
|
fc59619518 | ||
|
|
d01cfd2dd6 | ||
|
|
9563a4806a | ||
|
|
fa1be109e3 | ||
|
|
89a5a5928c | ||
|
|
7830378f23 | ||
|
|
043f616469 | ||
|
|
3bf631313a | ||
|
|
a416e2d4a8 | ||
|
|
53218cf1ce | ||
|
|
c5f0c8ed47 | ||
|
|
05e1c45adb | ||
|
|
e3787523c7 | ||
|
|
1c4e908b1b | ||
|
|
98b60b9833 | ||
|
|
71f8232ef0 | ||
|
|
0e4de407e3 | ||
|
|
802d732b3f | ||
|
|
373897f579 | ||
|
|
3ef72917c8 | ||
|
|
0d27125453 | ||
|
|
82c519b7d9 | ||
|
|
3c57a8a53c | ||
|
|
727ba6ccbf | ||
|
|
b32f1df9ec | ||
|
|
76b9afc6ec | ||
|
|
9fbf7baf76 |
No files matched your search
+7911
-1336
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+1101
-744
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+1156
-798
File diff suppressed because it is too large.
Load diff
@@ -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}")
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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
|
||||
|
||||
|
||||
+248
@@ -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
File diff suppressed because it is too large.
Load diff
+216
-154
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
)
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
[](https://github.com/87owo/PYAS/releases/latest)
|
||||
[](https://github.com/87owo/PYAS/stargazers)
|
||||
[](https://github.com/87owo/PYAS/network/members)
|
||||
[](https://github.com/87owo/PYAS)
|
||||
[](https://www.python.org/)
|
||||
[](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.
|
||||

|
||||
|
||||
```
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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 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>
|
||||
Reference in new issue
Block a user