ai-hacking-defense 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ai_hacking_defense-0.1.0/LICENSE +21 -0
- ai_hacking_defense-0.1.0/PKG-INFO +41 -0
- ai_hacking_defense-0.1.0/README.md +20 -0
- ai_hacking_defense-0.1.0/ai_hacking_defense.egg-info/PKG-INFO +41 -0
- ai_hacking_defense-0.1.0/ai_hacking_defense.egg-info/SOURCES.txt +10 -0
- ai_hacking_defense-0.1.0/ai_hacking_defense.egg-info/dependency_links.txt +1 -0
- ai_hacking_defense-0.1.0/ai_hacking_defense.egg-info/entry_points.txt +2 -0
- ai_hacking_defense-0.1.0/ai_hacking_defense.egg-info/top_level.txt +1 -0
- ai_hacking_defense-0.1.0/en/README_EN.md +20 -0
- ai_hacking_defense-0.1.0/pyproject.toml +32 -0
- ai_hacking_defense-0.1.0/scan.py +775 -0
- ai_hacking_defense-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AI Hacking Auto-Defense
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ai-hacking-defense
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Non-intrusive, zero-exfiltration personal security self-audit for the AI-hacking era.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/ftlord7/ai-hacking-defense
|
|
7
|
+
Keywords: security,audit,privacy,ai-agent,hardening,self-audit,zero-exfiltration
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: MacOS
|
|
13
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Security
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# ๐ก๏ธ AI-Hacking Auto-Defense Scanner (MVP v0.1)
|
|
23
|
+
A **non-intrusive, zero-exfiltration** personal security self-audit for macOS.
|
|
24
|
+
|
|
25
|
+
## Why
|
|
26
|
+
AI-automated attacks now hit individuals, not just enterprises. This tool shows your
|
|
27
|
+
security score and exactly what to fix โ without attacking anything and without
|
|
28
|
+
sending a single byte off your machine.
|
|
29
|
+
|
|
30
|
+
## Principles
|
|
31
|
+
- **Non-intrusive**: reads local settings/permissions/git/network only. No exploiting.
|
|
32
|
+
- **Zero-exfiltration**: 100% local. No network calls (no socket/urllib/requests imports โ verify it yourself).
|
|
33
|
+
- **No value leakage**: secret *values* are never recorded โ only their location.
|
|
34
|
+
- **Zero dependencies**: Python standard library only โ easy single-binary / pip install.
|
|
35
|
+
|
|
36
|
+
## Install & run
|
|
37
|
+
pip install ai-hacking-defense
|
|
38
|
+
ai-hacking-defense # or: python3 scan.py โ security_report.html
|
|
39
|
+
|
|
40
|
+
## Checks: credentials ยท network ยท OS hardening ยท AI-agent risk ยท backup
|
|
41
|
+
## License: open-core (free core, paid Pro). Zero-exfil is verifiable in source.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# ๐ก๏ธ AI-Hacking Auto-Defense Scanner (MVP v0.1)
|
|
2
|
+
A **non-intrusive, zero-exfiltration** personal security self-audit for macOS.
|
|
3
|
+
|
|
4
|
+
## Why
|
|
5
|
+
AI-automated attacks now hit individuals, not just enterprises. This tool shows your
|
|
6
|
+
security score and exactly what to fix โ without attacking anything and without
|
|
7
|
+
sending a single byte off your machine.
|
|
8
|
+
|
|
9
|
+
## Principles
|
|
10
|
+
- **Non-intrusive**: reads local settings/permissions/git/network only. No exploiting.
|
|
11
|
+
- **Zero-exfiltration**: 100% local. No network calls (no socket/urllib/requests imports โ verify it yourself).
|
|
12
|
+
- **No value leakage**: secret *values* are never recorded โ only their location.
|
|
13
|
+
- **Zero dependencies**: Python standard library only โ easy single-binary / pip install.
|
|
14
|
+
|
|
15
|
+
## Install & run
|
|
16
|
+
pip install ai-hacking-defense
|
|
17
|
+
ai-hacking-defense # or: python3 scan.py โ security_report.html
|
|
18
|
+
|
|
19
|
+
## Checks: credentials ยท network ยท OS hardening ยท AI-agent risk ยท backup
|
|
20
|
+
## License: open-core (free core, paid Pro). Zero-exfil is verifiable in source.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ai-hacking-defense
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Non-intrusive, zero-exfiltration personal security self-audit for the AI-hacking era.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/ftlord7/ai-hacking-defense
|
|
7
|
+
Keywords: security,audit,privacy,ai-agent,hardening,self-audit,zero-exfiltration
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: MacOS
|
|
13
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Security
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# ๐ก๏ธ AI-Hacking Auto-Defense Scanner (MVP v0.1)
|
|
23
|
+
A **non-intrusive, zero-exfiltration** personal security self-audit for macOS.
|
|
24
|
+
|
|
25
|
+
## Why
|
|
26
|
+
AI-automated attacks now hit individuals, not just enterprises. This tool shows your
|
|
27
|
+
security score and exactly what to fix โ without attacking anything and without
|
|
28
|
+
sending a single byte off your machine.
|
|
29
|
+
|
|
30
|
+
## Principles
|
|
31
|
+
- **Non-intrusive**: reads local settings/permissions/git/network only. No exploiting.
|
|
32
|
+
- **Zero-exfiltration**: 100% local. No network calls (no socket/urllib/requests imports โ verify it yourself).
|
|
33
|
+
- **No value leakage**: secret *values* are never recorded โ only their location.
|
|
34
|
+
- **Zero dependencies**: Python standard library only โ easy single-binary / pip install.
|
|
35
|
+
|
|
36
|
+
## Install & run
|
|
37
|
+
pip install ai-hacking-defense
|
|
38
|
+
ai-hacking-defense # or: python3 scan.py โ security_report.html
|
|
39
|
+
|
|
40
|
+
## Checks: credentials ยท network ยท OS hardening ยท AI-agent risk ยท backup
|
|
41
|
+
## License: open-core (free core, paid Pro). Zero-exfil is verifiable in source.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
scan.py
|
|
5
|
+
ai_hacking_defense.egg-info/PKG-INFO
|
|
6
|
+
ai_hacking_defense.egg-info/SOURCES.txt
|
|
7
|
+
ai_hacking_defense.egg-info/dependency_links.txt
|
|
8
|
+
ai_hacking_defense.egg-info/entry_points.txt
|
|
9
|
+
ai_hacking_defense.egg-info/top_level.txt
|
|
10
|
+
en/README_EN.md
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
scan
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# ๐ก๏ธ AI-Hacking Auto-Defense Scanner (MVP v0.1)
|
|
2
|
+
A **non-intrusive, zero-exfiltration** personal security self-audit for macOS.
|
|
3
|
+
|
|
4
|
+
## Why
|
|
5
|
+
AI-automated attacks now hit individuals, not just enterprises. This tool shows your
|
|
6
|
+
security score and exactly what to fix โ without attacking anything and without
|
|
7
|
+
sending a single byte off your machine.
|
|
8
|
+
|
|
9
|
+
## Principles
|
|
10
|
+
- **Non-intrusive**: reads local settings/permissions/git/network only. No exploiting.
|
|
11
|
+
- **Zero-exfiltration**: 100% local. No network calls (no socket/urllib/requests imports โ verify it yourself).
|
|
12
|
+
- **No value leakage**: secret *values* are never recorded โ only their location.
|
|
13
|
+
- **Zero dependencies**: Python standard library only โ easy single-binary / pip install.
|
|
14
|
+
|
|
15
|
+
## Install & run
|
|
16
|
+
pip install ai-hacking-defense
|
|
17
|
+
ai-hacking-defense # or: python3 scan.py โ security_report.html
|
|
18
|
+
|
|
19
|
+
## Checks: credentials ยท network ยท OS hardening ยท AI-agent risk ยท backup
|
|
20
|
+
## License: open-core (free core, paid Pro). Zero-exfil is verifiable in source.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ai-hacking-defense"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Non-intrusive, zero-exfiltration personal security self-audit for the AI-hacking era."
|
|
9
|
+
readme = "en/README_EN.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
keywords = ["security", "audit", "privacy", "ai-agent", "hardening", "self-audit", "zero-exfiltration"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: End Users/Desktop",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: MacOS",
|
|
19
|
+
"Operating System :: Microsoft :: Windows",
|
|
20
|
+
"Operating System :: POSIX :: Linux",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Topic :: Security",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/ftlord7/ai-hacking-defense"
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
ai-hacking-defense = "scan:main"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools]
|
|
32
|
+
py-modules = ["scan"]
|
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""AI Hacking Auto-Defense Scanner / AI ํดํน ์๋ ๋์ ์๋ฃจ์
(MVP v0.1)
|
|
4
|
+
Non-intrusive, zero-exfiltration personal security self-audit.
|
|
5
|
+
- Non-intrusive: reads local settings/permissions/git/network only. No exploiting.
|
|
6
|
+
- Zero-exfiltration: 100% local. No network calls (no socket/urllib/requests imports).
|
|
7
|
+
- No value leakage: secret VALUES are never recorded โ only location/count.
|
|
8
|
+
- Zero deps: Python stdlib only โ single-binary / pip friendly.
|
|
9
|
+
Usage: python3 scan.py [--lang ko|en] [--home PATH] [--json out.json] [--html report.html]
|
|
10
|
+
"""
|
|
11
|
+
import os, re, json, subprocess, argparse, datetime, html, getpass, sys
|
|
12
|
+
|
|
13
|
+
HOME = os.path.expanduser('~')
|
|
14
|
+
OS = 'mac' if sys.platform == 'darwin' else 'win' if sys.platform.startswith('win') else 'linux'
|
|
15
|
+
|
|
16
|
+
# โโ i18n ๋ฉ์์ง ์นดํ๋ก๊ทธ โโ
|
|
17
|
+
AREA = {
|
|
18
|
+
'cred': {'ko': '์๊ฒฉ์ฆ๋ช
', 'en': 'Credentials'},
|
|
19
|
+
'net': {'ko': '๋คํธ์ํฌ', 'en': 'Network'},
|
|
20
|
+
'os': {'ko': 'OS', 'en': 'OS'},
|
|
21
|
+
'ai': {'ko': 'AI์์ด์ ํธ', 'en': 'AI-Agent'},
|
|
22
|
+
'backup': {'ko': '๋ฐฑ์
', 'en': 'Backup'},
|
|
23
|
+
}
|
|
24
|
+
MSG = {
|
|
25
|
+
'git_cred': {
|
|
26
|
+
'ko': ('git์ ์๊ฒฉ์ฆ๋ช
ํ์ผ ์ถ์ ๋จ', '{n}๊ฐ ์ ์ฅ์์ ํ ํฐ/ํค/.env๊ฐ ์ปค๋ฐ๋จ: {d}',
|
|
27
|
+
'git rm --cached <ํ์ผ> ํ .gitignore ์ถ๊ฐ. ๊ณต๊ฐ ์ ์ฅ์๋ฉด ์ฆ์ ํค ์ฌ๋ฐ๊ธ(๋กํ
์ด์
).'),
|
|
28
|
+
'en': ('Credential files tracked in git', 'Tokens/keys/.env committed in {n} repo(s): {d}',
|
|
29
|
+
'Run git rm --cached <file>, add to .gitignore. If public, rotate the keys immediately.')},
|
|
30
|
+
'ssh_perm': {
|
|
31
|
+
'ko': ('SSH ๊ฐ์ธํค ๊ถํ ๊ณผ๋ค ({f})', '~/.ssh/{f} ๊ถํ {mode} โ ๋ค๋ฅธ ์ฌ์ฉ์ ์ ๊ทผ ๊ฐ๋ฅ', 'chmod 600 ~/.ssh/{f}'),
|
|
32
|
+
'en': ('SSH private key over-permissive ({f})', '~/.ssh/{f} is {mode} โ accessible by others', 'chmod 600 ~/.ssh/{f}')},
|
|
33
|
+
'hist_key': {
|
|
34
|
+
'ko': ('์
ธ ํ์คํ ๋ฆฌ์ ํค ํ๋ฌธ ์์ฌ ({h})', '~/{h}์ API ํค ํํ {n}๊ฑด (๊ฐ ๋ฏธํ์)',
|
|
35
|
+
'ํด๋น ์ค ์ญ์ ํ ํค ๋กํ
์ด์
. ํค๋ ๋ช
๋ นํ ๋์ ํ๊ฒฝ๋ณ์/ํ์ผ๋ก.'),
|
|
36
|
+
'en': ('Plaintext API keys in shell history ({h})', '{n} API-key-like string(s) in ~/{h} (values hidden)',
|
|
37
|
+
'Delete those lines and rotate keys. Pass secrets via env/file, not the command line.')},
|
|
38
|
+
'open_port': {
|
|
39
|
+
'ko': ('์ธ๋ถ ๊ณต๊ฐ(0.0.0.0) ๋ฆฌ์ค๋ ํฌํธ', '๋ชจ๋ ์ธํฐํ์ด์ค์ ์ด๋ฆฐ ํฌํธ: {d}',
|
|
40
|
+
'๋ถํ์ํ ์๋น์ค๋ ์ข
๋ฃ. ํ์ํ๋ฉด 127.0.0.1(๋ก์ปฌ)๋ก๋ง ๋ฐ์ธ๋ฉํ๊ฑฐ๋ ๋ฐฉํ๋ฒฝ ์ ํ.'),
|
|
41
|
+
'en': ('Publicly-bound (0.0.0.0) listening ports', 'Ports open on all interfaces: {d}',
|
|
42
|
+
'Stop unneeded services. Bind to 127.0.0.1 only, or restrict via firewall.')},
|
|
43
|
+
'fw_off': {
|
|
44
|
+
'ko': ('๋ฐฉํ๋ฒฝ ๋นํ์ฑ', '๋ฐฉํ๋ฒฝ์ด ๊บผ์ ธ ์์ด ์ธ๋ถ ์ ๊ทผ์ด ๊ฑธ๋ฌ์ง์ง ์์', '๋ฐฉํ๋ฒฝ์ ์ผ์ธ์ โ mac: ์์คํ
์ค์ >๋คํธ์ํฌ>๋ฐฉํ๋ฒฝ / Windows: Defender ๋ฐฉํ๋ฒฝ / Linux: sudo ufw enable.'),
|
|
45
|
+
'en': ('Firewall disabled', 'The firewall is off, so inbound access is unfiltered', 'Turn the firewall on โ mac: System Settings>Network>Firewall / Windows: Defender Firewall / Linux: sudo ufw enable.')},
|
|
46
|
+
'filevault': {
|
|
47
|
+
'ko': ('๋์คํฌ ์ํธํ ๊บผ์ง', '๋ถ์คยท๋๋ ์ ๋์คํฌ ๋ด์ฉ์ด ํ๋ฌธ์ผ๋ก ๋
ธ์ถ๋จ', '๋์คํฌ ์ํธํ๋ฅผ ์ผ์ธ์ โ mac: FileVault / Windows: BitLocker / Linux: LUKS.'),
|
|
48
|
+
'en': ('Disk encryption off', 'Disk is readable in plaintext if lost/stolen', 'Turn on disk encryption โ mac: FileVault / Windows: BitLocker / Linux: LUKS.')},
|
|
49
|
+
'autologin': {
|
|
50
|
+
'ko': ('์๋ ๋ก๊ทธ์ธ ํ์ฑ', '๋ถํ
์ ์ํธ ์์ด ๋ก๊ทธ์ธ๋จ', '์์คํ
์ค์ > ์ฌ์ฉ์ ๋ฐ ๊ทธ๋ฃน > ์๋ ๋ก๊ทธ์ธ ๋๊ธฐ.'),
|
|
51
|
+
'en': ('Automatic login enabled', 'Boots straight in without a password', 'System Settings > Users & Groups > turn off automatic login.')},
|
|
52
|
+
'screenlock': {
|
|
53
|
+
'ko': ('ํ๋ฉด๋ณดํธ๊ธฐ ํ ์ํธ ๋ฏธ์๊ตฌ', '์ ๊ธ ํ ์ํธ ์์ด ๋ณต๊ท ๊ฐ๋ฅ', '์์คํ
์ค์ > ์ ๊ธ ํ๋ฉด > ์ํธ ์๊ตฌ = ์ฆ์.'),
|
|
54
|
+
'en': ('No password after screensaver', 'Resumes without a password', 'System Settings > Lock Screen > Require password = immediately.')},
|
|
55
|
+
'os_ver': {
|
|
56
|
+
'ko': ('macOS {v}', 'ํ์ฌ OS ๋ฒ์ ', 'softwareupdate -l ๋ก ๋ณด์ ์
๋ฐ์ดํธ ํ์ธยท์ ์ฉ.'),
|
|
57
|
+
'en': ('macOS {v}', 'Current OS version', 'Check/apply security updates: softwareupdate -l.')},
|
|
58
|
+
'ai_cfg': {
|
|
59
|
+
'ko': ('AI ๋๊ตฌ ์ค์ ์ ์๊ฒฉ์ฆ๋ช
๊ถํ ๊ณผ๋ค', '{n}๊ฑด: {d}', 'ํด๋น ํ์ผ chmod 600. MCP/๋๊ตฌ ํ ํฐ์ ์ต์๊ถํ์ผ๋ก.'),
|
|
60
|
+
'en': ('Over-permissive credentials in AI tool config', '{n} file(s): {d}', 'chmod 600 those files. Keep MCP/tool tokens least-privilege.')},
|
|
61
|
+
'ai_inject': {
|
|
62
|
+
'ko': ('AI ์์ด์ ํธ ํ๋กฌํํธ ์ธ์ ์
์ฃผ์', '์ธ๋ถ ์ฝํ
์ธ (์นยท๋ฉ์ผยท๋ฌธ์)๋ฅผ ์ฝ๋ AI ๋๊ตฌ๋ ์จ์ ์ง์์ ์์ ์ ์์',
|
|
63
|
+
'์ ๋ขฐ ๋ชป ํ ์ฝํ
์ธ ๋ฅผ ์์ด์ ํธ์ ๊ทธ๋๋ก ๋ฃ์ง ๋ง ๊ฒ. ์๋ ์คํ ๊ถํ ์ต์ํ.'),
|
|
64
|
+
'en': ('AI-agent prompt-injection awareness', 'AI tools that read external content can be tricked by hidden instructions',
|
|
65
|
+
'Do not feed untrusted content directly to agents. Minimize auto-execution permissions.')},
|
|
66
|
+
'no_backup': {
|
|
67
|
+
'ko': ('์ต๊ทผ ๋ฐฑ์
์์', '๋์ฌ์จ์ดยท๊ณ ์ฅ ์ ๋ณต๊ตฌ ๋ถ๊ฐ ์ํ', '์ ๊ธฐ ๋ฐฑ์
์ ์ผ์ธ์ โ mac: Time Machine / Windows: ํ์ผ ํ์คํ ๋ฆฌ / Linux: ๋ฐฑ์
๋๊ตฌ.'),
|
|
68
|
+
'en': ('No recent backup', 'No recovery if ransomware/hardware failure hits', 'Enable regular backups โ mac: Time Machine / Windows: File History / Linux: a backup tool.')},
|
|
69
|
+
'backup_repo': {
|
|
70
|
+
'ko': ('์ ์ฅ์ ์๋ ๋ฐฑ์
๊ฐ๋ ์ค (๋ง์ง๋ง ์ปค๋ฐ {m}๋ถ ์ )', '์ฝ๋ยท๊ธฐ๋ก์ ๋งค์๊ฐ ์ ์ฅ์ ์ปค๋ฐ์ผ๋ก ๋ฐฑ์
๋จ โ ๋จ ๊ฐ์ ๊ธฐ๊ธฐ ์์ด๋ผ ๋์คํฌ ์์ค์๋ ์ฝํจ', '๊ธฐ๊ธฐ ๋ฐ ์ฌ๋ณธ(Time Machineยทํด๋ผ์ฐ๋)์ ์ถ๊ฐํ๋ฉด ๋ ์์ ํฉ๋๋ค. ๋ณต์ ๋ฆฌํ์ค์ ์์ง ์ ํ์ด์.'),
|
|
71
|
+
'en': ('Repository auto-backup active (last commit {m}m ago)', 'Code/records backed up hourly via repo commits โ same-disk only, weak against disk loss', 'Add an off-device copy (Time Machine/cloud). Restore rehearsal not yet done.')},
|
|
72
|
+
}
|
|
73
|
+
UI = {
|
|
74
|
+
'title': {'ko': 'AI ํดํน ์๋ ๋์ ์๋ฃจ์
', 'en': 'AI-Hacking Auto-Defense'},
|
|
75
|
+
'rep': {'ko': '๋ณด์ ๋ฆฌํฌํธ', 'en': 'Security Report'},
|
|
76
|
+
'of': {'ko': '๋์ ๊ธฐ๊ธฐ ๋ณด์ ๋ฆฌํฌํธ', 'en': "'s device"},
|
|
77
|
+
'subtail': {'ko': '๋น์นจํฌ ์๊ฐ์ง๋จ', 'en': 'non-intrusive self-audit'},
|
|
78
|
+
'sclab': {'ko': '/ 100 ๋ณด์์ ์', 'en': '/ 100 security score'},
|
|
79
|
+
'high': {'ko': '์ํ', 'en': 'Critical'},
|
|
80
|
+
'med': {'ko': '์ฃผ์', 'en': 'Warning'},
|
|
81
|
+
'clean': {'ko': 'โ
๋ฐ๊ฒฌ๋ ์ทจ์ฝ์ ์์ โ ์์ ๋ฌด์ฅ ์ํ์
๋๋ค.', 'en': 'โ
No issues found โ fully hardened.'},
|
|
82
|
+
'priv': {'ko': '๐ ์ ๋ก ์ ์ถ ๋ณด์ฆ: ์ด ์ค์บ์ 100% ๋น์ ์ ๊ธฐ๊ธฐ์์๋ง ์คํ๋์์ต๋๋ค. ์ด๋ค ๋ฐ์ดํฐยท์ํฌ๋ฆฟ๋ ์ธ๋ถ๋ก ์ ์ก๋์ง ์์์ต๋๋ค(๋คํธ์ํฌ ์ ์ก 0). ์ํฌ๋ฆฟ ๊ฐ์ ๊ธฐ๋ก๋์ง ์์ต๋๋ค โ ์์นยท๊ฑด์๋ง ํ์ํฉ๋๋ค.',
|
|
83
|
+
'en': '๐ Zero-exfiltration guarantee: this scan ran 100% on your device. No data or secret was ever transmitted (zero network). Secret values are never recorded โ only location/count.'},
|
|
84
|
+
'done': {'ko': '๋ณด์์ ์ {s}/100 ยท ์ํ {h}ยท์ฃผ์ {m} ยท ๋ฐ๊ฒฌ {n}๊ฑด', 'en': 'Security score {s}/100 ยท critical {h} ยท warning {m} ยท {n} findings'},
|
|
85
|
+
'repline': {'ko': '๋ฆฌํฌํธ', 'en': 'Report'},
|
|
86
|
+
'zero': {'ko': '๐ ์ ๋ก ์ ์ถ: ๋คํธ์ํฌ ์ ์ก 0', 'en': '๐ Zero-exfiltration: no network transmission'},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# โโ "์ ์ค์ํ๊ฐ" (ํธ๊ธฐ์ฌ ์ถฉ์กฑ ๊ต์ก ์ ๋ณดยท์์ญ๋ณ) โโ
|
|
90
|
+
WHY = {
|
|
91
|
+
'cred': {'ko': "๊ณต๊ฒฉ์๊ฐ ๊ฐ์ฅ ๋จผ์ ๋
ธ๋ฆฌ๋ ๊ฑด '์ด๋ฏธ ์ ์ถ๋ ์ด์ '์
๋๋ค. ์ฝ๋ยท๊ธฐ๋ก์ ๋จ์ ํค ํ๋๋ฉด ๋ก๊ทธ์ธ ๊ณผ์ ์ ํต์งธ๋ก ๊ฑด๋๋๋๋ค.",
|
|
92
|
+
'en': "Attackers reach for already-leaked keys first โ one key left in code or history skips the entire login."},
|
|
93
|
+
'net': {'ko': "์ด๋ฆฐ ํฌํธ๋ ์ง์ ์ด๋ฆฐ ์ฐฝ๋ฌธ์
๋๋ค. ์๋ํ๋ ๋ด์ด 24์๊ฐ ์ ์ธ๊ณ IP๋ฅผ ํ์ผ๋ฉฐ ์ด๋ฆฐ ์ฐฝ๋ฌธ์ ์ฐพ์ต๋๋ค.",
|
|
94
|
+
'en': "An open port is an open window. Automated bots sweep the whole internet 24/7 looking for one."},
|
|
95
|
+
'os': {'ko': "๊ธฐ๊ธฐ๋ฅผ ์์ด๋ฒ๋ฆฌ๋ ์๊ฐ, ์ํธํยท์ ๊ธ์ด ์์ผ๋ฉด ์์ ๋ชจ๋ ๊ฒ์ด ๊ทธ๋๋ก ๋จ์ ์์ ๋ค์ด๊ฐ๋๋ค.",
|
|
96
|
+
'en': "The moment a device is lost, without encryption or a lock everything inside is simply handed over."},
|
|
97
|
+
'ai': {'ko': "AI ๋๊ตฌ๋ ๋น์ ๋์ ํ๋ํฉ๋๋ค. ๊ทธ ๊ถํยท์ด์ ๊ฐ ์๋ฉด ๊ณต๊ฒฉ์๋ 'AI๋ฅผ ํตํด' ๋น์ ํ์ธ๋ฅผ ํฉ๋๋ค.",
|
|
98
|
+
'en': "AI tools act on your behalf. If their keys leak, an attacker impersonates you through the AI."},
|
|
99
|
+
'backup':{'ko': "๋์ฌ์จ์ด๋ ๋น์ ์ ๋ง์ง ์์ต๋๋ค โ ๋น์ ์ ๋ฐ์ดํฐ๋ฅผ ์ธ์ง๋ก ์ก์ต๋๋ค. ๋ฐฑ์
๋ง์ด ๋ชธ๊ฐ์ ๋ฌด๋ ฅํํฉ๋๋ค.",
|
|
100
|
+
'en': "Ransomware doesn't lock you out โ it takes your data hostage. Only a backup makes the ransom worthless."},
|
|
101
|
+
}
|
|
102
|
+
# โโ ์์ญ๋ณ ์ค์ ์ ๊ฒ ์คํ
(๋ผ์ด๋ธ ์ค์บ ํผ๋์ฉยท์ค์ ์ํ ํญ๋ชฉ) โโ
|
|
103
|
+
CHECKS = {
|
|
104
|
+
'cred': {'ko':['Git ์ ์ฅ์ ํ ํฐยทํค ์ค์บ','~/.ssh ๊ฐ์ธํค ๊ถํ ์ ๊ฒ','์
ธ ํ์คํ ๋ฆฌ APIํค ํ์ง','.env ์๊ฒฉ์ฆ๋ช
๋
ธ์ถ ํ์ธ','OAuth ํ ํฐ ํ์ผ ๊ถํ ์ ๊ฒ','.gitignore ์ํฌ๋ฆฟ ์ปค๋ฒ๋ฆฌ์ง ํ์ธ','ํ๋์ฝ๋ฉ ๋น๋ฐ๋ฒํธ ํจํด ์ค์บ','ํด๋ผ์ฐ๋ ํค ํ์ผ ์ ๊ฒ'],
|
|
105
|
+
'en':['Scanning git repos for tokens','Checking ~/.ssh key permissions','Detecting API keys in shell history','Checking .env credential exposure','Checking OAuth token file perms','Verifying .gitignore secret coverage','Scanning for hardcoded passwords','Checking cloud key files']},
|
|
106
|
+
'net': {'ko':['๋ฆฌ์ค๋ ํฌํธ ์ค์บ','0.0.0.0 ๊ณต๊ฐ ๋ฐ์ธ๋ฉ ์ ๊ฒ','๋ฐฉํ๋ฒฝ ์ํ ํ์ธ','์๊ฒฉ ์ ์(SSH/RDP) ํฌํธ ์ ๊ฒ','๊ณต์ ์๋น์ค ๋
ธ์ถ ํ์ธ','์์๋ฐ์ด๋ ์ฐ๊ฒฐ ์ ๊ฒ'],
|
|
107
|
+
'en':['Scanning listening ports','Checking 0.0.0.0 public binds','Verifying firewall state','Checking remote (SSH/RDP) ports','Checking exposed shared services','Reviewing outbound connections']},
|
|
108
|
+
'os': {'ko':['๋์คํฌ ์ํธํ ์ํ ํ์ธ','์๋ ๋ก๊ทธ์ธ ์ค์ ์ ๊ฒ','ํ๋ฉด ์ ๊ธ ์ ์ฑ
ํ์ธ','OS ๋ณด์ ํจ์น ์ํ','๊ด๋ฆฌ์ ๊ณ์ ์ ๊ฒ','๋ถํ
๋ณด์ ์ค์ ํ์ธ'],
|
|
109
|
+
'en':['Checking disk encryption','Checking auto-login setting','Verifying screen-lock policy','Checking OS patch level','Reviewing admin accounts','Checking secure-boot settings']},
|
|
110
|
+
'ai': {'ko':['AI ๋๊ตฌ ์ค์ ๊ถํ ์ ๊ฒ','MCP/ํ ํฐ ๋
ธ์ถ ํ์ธ','ํ๋กฌํํธ ์ธ์ ์
๋
ธ์ถ๋ฉด ๋ถ์','์์ด์ ํธ ์๋์คํ ๊ถํ ์ ๊ฒ','์ธ๋ถ ์ฝํ
์ธ ์ ๋ขฐ ๊ฒฝ๊ณ ํ์ธ','์์ด์ ํธ ๋ก๊ทธ ๋ฏผ๊ฐ์ ๋ณด ์ ๊ฒ'],
|
|
111
|
+
'en':['Checking AI tool config perms','Checking MCP/token exposure','Analyzing prompt-injection surface','Reviewing agent auto-run perms','Checking external-content trust boundary','Scanning agent logs for secrets']},
|
|
112
|
+
'backup':{'ko':['๋ฐฑ์
์กด์ฌยท์ฃผ๊ธฐ ํ์ธ','๋์ฌ์จ์ด ๋ณต๊ตฌ ๋๋น ์ ๊ฒ','๋ฐฑ์
์ํธํ ํ์ธ','๋ณต์ ์ง์ ์ ํจ์ฑ ์ ๊ฒ'],
|
|
113
|
+
'en':['Checking backup presence','Checking ransomware recovery','Verifying backup encryption','Validating restore points']},
|
|
114
|
+
}
|
|
115
|
+
# โโ ๋์๋ณด๋ UI ๋ฌธ์์ด โโ
|
|
116
|
+
UISTR = {
|
|
117
|
+
'ko': {'brand':'AI ํดํน ์๋ ๋์','hero_sub':'๋์ ๊ธฐ๊ธฐ ๋ฉด์ญ ์ํ','immunity':'๋ฉด์ญ ์ ์',
|
|
118
|
+
'v_good':'์ํธ','v_warn':'์ฃผ์ ํ์','v_crit':'์ํ',
|
|
119
|
+
'vitals':'์์ญ๋ณ ๋ฐ์ดํ','monitor_title':'๐ ์ค์๊ฐ ์ค์บ ๋ชจ๋ํฐ ยท ์๋ ์ ๊ฒ ์ค','scanning':'์ค์บ ์ค','scandone':'์ ๊ฒ','sec_find':'๋ฐ๊ฒฌ ๋ชฉ๋ก','net_title':'๐ข ์ค์๊ฐ ๋ฐฉ์ด ๋คํธ์ํฌ','watching':'๊ฐ์ ์ค','live_watch':'์ค์๊ฐ ๊ฐ์ ์ค ยท 5๊ฐ ์์ญ ๋ณดํธ','all':'์ ์ฒด','crit':'์ํ','warn':'์ฃผ์','info':'์ ๋ณด',
|
|
120
|
+
'findings':'๊ฑด ๋ฐ๊ฒฌ','clean':'๋ฐ๊ฒฌ๋ ์ทจ์ฝ์ ์ด ์์ต๋๋ค. ์์ ๋ฌด์ฅ ์ํ์์.',
|
|
121
|
+
'why':'์ ์ค์ํ๊ฐ','fix':'๊ณ ์น๋ ๋ฒ','copy':'๋ณต์ฌ','copied':'๋ณต์ฌ๋จ',
|
|
122
|
+
'export':'๋ด๋ณด๋ด๊ธฐ','print':'์ธ์','local':'100% ๋ก์ปฌ ยท ์ธ๋ถ ์ ์ก 0',
|
|
123
|
+
'local_more':'์ด ์ง๋จ์ ๋น์ ์ ๊ธฐ๊ธฐ์์๋ง ์คํ๋ฉ๋๋ค. ํ์ผยท๋น๋ฐ๋ฒํธยท๋ฐ์ดํฐ๊ฐ ์ธ๋ถ๋ก ์ ์ก๋์ง ์์ต๋๋ค(๋คํธ์ํฌ ์ ์ก 0). ๋น๋ฐ๋ฒํธ ๊ฐ์ ๊ธฐ๋กํ์ง ์๊ณ , ์ํ์ด "์ด๋"์ ์๋์ง๋ง ์๋ ค์ค๋๋ค.',
|
|
124
|
+
'scanned':'์ง๋จ ์๊ฐ','whatscore':'์ ์๊ฐ ๋ฎ์์๋ก ๊ณต๊ฒฉ์ ๋
ธ์ถ๋ ํ์ด ๋ง๋ค๋ ๋ป์
๋๋ค. ํญ๋ชฉ์ ๋๋ฌ ํ๋์ฉ ๋ซ์ ๋ณด์ธ์.'},
|
|
125
|
+
'en': {'brand':'AI-Hacking Auto-Defense','hero_sub':"'s device immunity",'immunity':'Immunity score',
|
|
126
|
+
'v_good':'Healthy','v_warn':'Needs attention','v_crit':'At risk',
|
|
127
|
+
'vitals':'Vitals by area','monitor_title':'๐ Live scan monitor ยท auto-checking','scanning':'scanning','scandone':'checked','sec_find':'Findings','net_title':'๐ข Live defense network','watching':'LIVE','live_watch':'Live monitoring ยท 5 areas protected','all':'All','crit':'Critical','warn':'Warning','info':'Info',
|
|
128
|
+
'findings':'findings','clean':'No issues found. Your device is fully hardened.',
|
|
129
|
+
'why':'Why it matters','fix':'How to fix','copy':'Copy','copied':'Copied',
|
|
130
|
+
'export':'Export','print':'Print','local':'100% local ยท zero transmission',
|
|
131
|
+
'local_more':'This scan runs only on your device. No file, password, or data is ever sent out (zero network). Password values are never recorded โ we only tell you where a risk is.',
|
|
132
|
+
'scanned':'Scanned','whatscore':'A lower score means more gaps exposed to attack. Tap an item to close them one by one.'},
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
def run(cmd, timeout=8):
|
|
136
|
+
try:
|
|
137
|
+
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
138
|
+
return (r.stdout or '') + (r.stderr or '')
|
|
139
|
+
except Exception:
|
|
140
|
+
return ''
|
|
141
|
+
|
|
142
|
+
class Scan:
|
|
143
|
+
def __init__(self, home, lang='ko'):
|
|
144
|
+
self.home = home; self.lang = lang if lang in ('ko', 'en') else 'ko'
|
|
145
|
+
self.findings = []
|
|
146
|
+
def add(self, sev, area_key, msg_id, **fmt):
|
|
147
|
+
title, detail, fix = MSG[msg_id][self.lang]
|
|
148
|
+
self.findings.append({'sev': sev, 'area': AREA[area_key][self.lang], 'areaKey': area_key,
|
|
149
|
+
'title': title.format(**fmt), 'detail': detail.format(**fmt),
|
|
150
|
+
'fix': fix.format(**fmt), 'why': WHY[area_key][self.lang]})
|
|
151
|
+
|
|
152
|
+
def credentials(self):
|
|
153
|
+
repos = []
|
|
154
|
+
for dp, dns, fns in os.walk(self.home):
|
|
155
|
+
if dp[len(self.home):].count(os.sep) > 4: dns[:] = []; continue
|
|
156
|
+
dns[:] = [d for d in dns if d not in ('node_modules','Library','.Trash','.cache') and (not d.startswith('.') or d == '.git')]
|
|
157
|
+
if '.git' in dns: repos.append(dp); dns.remove('.git')
|
|
158
|
+
cred_pat = re.compile(r'(\.env($|\.)|/token[^/]*\.json$|secret[^/]*\.json$|\.pem$|\.key$|\.p12$|\.session$|id_rsa$|credentials?\.json$)', re.I)
|
|
159
|
+
hit = []
|
|
160
|
+
for r in repos[:60]:
|
|
161
|
+
out = run(['git','-C',r,'ls-files'])
|
|
162
|
+
bad = [l for l in out.splitlines() if cred_pat.search(l) and 'node_modules/' not in l]
|
|
163
|
+
if bad: hit.append((r, len(bad)))
|
|
164
|
+
if hit:
|
|
165
|
+
d = '; '.join(f"{os.path.basename(r)}({n})" for r,n in hit[:6])
|
|
166
|
+
self.add('HIGH','cred','git_cred', n=len(hit), d=d)
|
|
167
|
+
ssh = os.path.join(self.home, '.ssh')
|
|
168
|
+
if os.path.isdir(ssh):
|
|
169
|
+
for f in os.listdir(ssh):
|
|
170
|
+
p = os.path.join(ssh, f)
|
|
171
|
+
if f.startswith('id_') and not f.endswith('.pub') and os.path.isfile(p):
|
|
172
|
+
mode = oct(os.stat(p).st_mode & 0o777)[2:]
|
|
173
|
+
if mode[-1] in '1234567' or mode[-2] in '1234567':
|
|
174
|
+
self.add('HIGH','cred','ssh_perm', f=f, mode=mode)
|
|
175
|
+
hists = ['.zsh_history','.bash_history']
|
|
176
|
+
if OS == 'win': hists = [os.path.join('AppData','Roaming','Microsoft','Windows','PowerShell','PSReadLine','ConsoleHost_history.txt')]
|
|
177
|
+
for hist in hists:
|
|
178
|
+
p = os.path.join(self.home, hist)
|
|
179
|
+
if os.path.isfile(p):
|
|
180
|
+
try: txt = open(p, errors='ignore').read()
|
|
181
|
+
except Exception: continue
|
|
182
|
+
n = len(re.findall(r'(sk-[A-Za-z0-9]{20,}|AIza[A-Za-z0-9_\-]{30,}|xox[baprs]-[A-Za-z0-9\-]{10,}|ghp_[A-Za-z0-9]{30,}|Bearer [A-Za-z0-9._\-]{20,})', txt))
|
|
183
|
+
if n: self.add('MED','cred','hist_key', h=os.path.basename(hist), n=n)
|
|
184
|
+
|
|
185
|
+
def network(self):
|
|
186
|
+
public = set()
|
|
187
|
+
if OS == 'win':
|
|
188
|
+
for l in run(['netstat','-ano','-p','TCP']).splitlines():
|
|
189
|
+
m = re.search(r'(0\.0\.0\.0|\[::\]):(\d+)\s+.*LISTENING', l)
|
|
190
|
+
if m: public.add(m.group(2))
|
|
191
|
+
fw = run(['netsh','advfirewall','show','allprofiles','state'])
|
|
192
|
+
if 'ON' not in fw.upper(): self.add('HIGH','net','fw_off')
|
|
193
|
+
else:
|
|
194
|
+
cmd = ['lsof','-nP','-iTCP','-sTCP:LISTEN'] if OS == 'mac' else ['ss','-tlnH']
|
|
195
|
+
for l in run(cmd).splitlines():
|
|
196
|
+
m = re.search(r'(\*|0\.0\.0\.0|\[::\]|::):(\d+)', l)
|
|
197
|
+
if m:
|
|
198
|
+
proc = l.split()[0] if (OS=='mac' and l.split()) else ''
|
|
199
|
+
public.add(f'{m.group(2)}/{proc}' if proc else m.group(2))
|
|
200
|
+
if OS == 'mac':
|
|
201
|
+
if 'enabled' not in run(['/usr/libexec/ApplicationFirewall/socketfilterfw','--getglobalstate']).lower():
|
|
202
|
+
self.add('HIGH','net','fw_off')
|
|
203
|
+
else: # linux
|
|
204
|
+
ufw = run(['ufw','status'])
|
|
205
|
+
if ufw and 'inactive' in ufw.lower(): self.add('HIGH','net','fw_off')
|
|
206
|
+
if public: self.add('MED','net','open_port', d=', '.join(sorted(public)[:10]))
|
|
207
|
+
|
|
208
|
+
def os_hardening(self):
|
|
209
|
+
if OS == 'mac':
|
|
210
|
+
if 'On' not in run(['fdesetup','status']): self.add('HIGH','os','filevault')
|
|
211
|
+
al = run(['defaults','read','/Library/Preferences/com.apple.loginwindow','autoLoginUser'])
|
|
212
|
+
if al.strip() and 'does not exist' not in al: self.add('HIGH','os','autologin')
|
|
213
|
+
if run(['defaults','read','com.apple.screensaver','askForPassword']).strip() == '0': self.add('MED','os','screenlock')
|
|
214
|
+
v = run(['sw_vers','-productVersion']).strip()
|
|
215
|
+
if v: self.add('INFO','os','os_ver', v=v)
|
|
216
|
+
elif OS == 'win':
|
|
217
|
+
bde = run(['manage-bde','-status','C:'])
|
|
218
|
+
if bde and 'Protection On' not in bde: self.add('HIGH','os','filevault')
|
|
219
|
+
au = run(['reg','query','HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon','/v','AutoAdminLogon'])
|
|
220
|
+
if re.search(r'AutoAdminLogon\s+REG_SZ\s+1', au): self.add('HIGH','os','autologin')
|
|
221
|
+
self.add('INFO','os','os_ver', v=f"Windows {run(['cmd','/c','ver']).strip()[:40]}")
|
|
222
|
+
else: # linux
|
|
223
|
+
enc = run(['lsblk','-o','NAME,TYPE'])
|
|
224
|
+
if enc and 'crypt' not in enc: self.add('HIGH','os','filevault')
|
|
225
|
+
try: rel = open('/etc/os-release').read()
|
|
226
|
+
except Exception: rel = ''
|
|
227
|
+
m = re.search(r'PRETTY_NAME="?([^"\n]+)', rel)
|
|
228
|
+
self.add('INFO','os','os_ver', v=(m.group(1) if m else 'Linux'))
|
|
229
|
+
|
|
230
|
+
def ai_agent(self):
|
|
231
|
+
hot = []
|
|
232
|
+
for cfg in ('.claude','.config/claude','.cursor','.aws','.config'):
|
|
233
|
+
p = os.path.join(self.home, cfg)
|
|
234
|
+
if os.path.isdir(p):
|
|
235
|
+
for dp, dns, fns in os.walk(p):
|
|
236
|
+
if dp.count(os.sep) - p.count(os.sep) > 2: dns[:] = []; continue
|
|
237
|
+
for f in fns:
|
|
238
|
+
if re.search(r'(token|secret|credential|\.env|key)', f, re.I) and f.endswith(('.json','.env','.key','.pem')):
|
|
239
|
+
fp = os.path.join(dp, f)
|
|
240
|
+
try: mode = oct(os.stat(fp).st_mode & 0o777)[2:]
|
|
241
|
+
except Exception: continue
|
|
242
|
+
if mode[-1] in '1234567' or mode[-2] in '1234567': hot.append(fp.replace(self.home,'~'))
|
|
243
|
+
if hot: self.add('MED','ai','ai_cfg', n=len(hot), d=', '.join(hot[:5]))
|
|
244
|
+
self.add('INFO','ai','ai_inject')
|
|
245
|
+
|
|
246
|
+
def _repo_backup_recent(self, max_age_h=3):
|
|
247
|
+
"""์ ์ฅ์ ์๋ ์ปค๋ฐ ๋ฐฑ์
์ค์ฆ โ SECSCAN_BACKUP_REPOS(์ฝ๋ก ๊ตฌ๋ถ) ๊ฒฝ๋ก์ cwd(git repo์ผ
|
|
248
|
+
๋๋ง) ์ค git ์ต๊ทผ ์ปค๋ฐ์ด max_age_h ์ด๋ด๋ฉด (๊ฒฝ๋ก, ๊ฒฝ๊ณผ๋ถ) ๋ฐํ. ์ฃผ๊ธฐ ์ปค๋ฐ์ ๋ฐฑ์
์ผ๋ก
|
|
249
|
+
์ธ์ ('Time Machine๋ง = ๋ฐฑ์
' ์ ์๊ฐ ๋ณ์ '์ต๊ทผ ๋ฐฑ์
์์' ์คํ ์ ์ ). ๊ธฐ๋ณธ๊ฐ์ cwd๋ฟ โ
|
|
250
|
+
์ด๋ค ํ๊ฒฝ ๊ฒฝ๋ก๋ ์ฝ๋์ ๋์ง ์๋๋ค(์ด์ฉ์ธก์ด env/--backup-repos๋ก ์ฃผ์
)."""
|
|
251
|
+
repos = [p for p in os.environ.get('SECSCAN_BACKUP_REPOS','').split(':') if p]
|
|
252
|
+
repos += [os.getcwd()]
|
|
253
|
+
for r in repos:
|
|
254
|
+
if not os.path.isdir(os.path.join(os.path.expanduser(r), '.git')): continue
|
|
255
|
+
ts = run(['git','-C',os.path.expanduser(r),'log','-1','--format=%ct'])
|
|
256
|
+
try: age_s = datetime.datetime.now().timestamp() - int(ts.strip())
|
|
257
|
+
except Exception: continue
|
|
258
|
+
if age_s <= max_age_h*3600: return r, int(age_s//60)
|
|
259
|
+
return None, None
|
|
260
|
+
|
|
261
|
+
def backup(self):
|
|
262
|
+
repo, age_min = self._repo_backup_recent()
|
|
263
|
+
if OS == 'mac':
|
|
264
|
+
tm = run(['tmutil','latestbackup'])
|
|
265
|
+
tm_ok = not ('no' in tm.lower() or not tm.strip() or 'Error' in tm)
|
|
266
|
+
if tm_ok: pass
|
|
267
|
+
elif repo: self.add('INFO','backup','backup_repo', m=age_min)
|
|
268
|
+
else: self.add('MED','backup','no_backup')
|
|
269
|
+
elif OS == 'win':
|
|
270
|
+
fh = run(['cmd','/c','fhmanagew.exe','query'])
|
|
271
|
+
if not fh or 'not configured' in fh.lower():
|
|
272
|
+
if repo: self.add('INFO','backup','backup_repo', m=age_min)
|
|
273
|
+
else: self.add('MED','backup','no_backup')
|
|
274
|
+
# linux: ํ์ค ๋ฐฑ์
๋ฉ์ปค๋์ฆ ์์ โ ์คํ ๋ฐฉ์ง ์ํด ๋ฏธํ์
|
|
275
|
+
|
|
276
|
+
def run_all(self):
|
|
277
|
+
for fn in (self.credentials, self.network, self.os_hardening, self.ai_agent, self.backup):
|
|
278
|
+
try: fn()
|
|
279
|
+
except Exception: pass
|
|
280
|
+
H = sum(1 for f in self.findings if f['sev']=='HIGH')
|
|
281
|
+
M = sum(1 for f in self.findings if f['sev']=='MED')
|
|
282
|
+
return {'ts': datetime.datetime.now().strftime('%Y-%m-%d %H:%M'), 'lang': self.lang, 'os': OS,
|
|
283
|
+
'user': getpass.getuser(), 'score': max(0, 100-H*12-M*5), 'high': H, 'med': M, 'findings': self.findings}
|
|
284
|
+
|
|
285
|
+
_DASHBOARD = r"""<!doctype html><html lang="__L__"><head><meta charset="utf-8">
|
|
286
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"><title>AI-Hacking Auto-Defense</title>
|
|
287
|
+
<style>
|
|
288
|
+
:root{--ink:#0E1922;--surf:#15232E;--surf2:#1B2C38;--line:#283a46;--text:#EAF1F4;--muted:#90A4AF;--faint:#62788699;
|
|
289
|
+
--crit:#FF6B6B;--warn:#FFC24B;--good:#36D399;--info:#5AB0FF;--r:18px;
|
|
290
|
+
--sans:-apple-system,BlinkMacSystemFont,"SF Pro Text","Segoe UI",system-ui,sans-serif;
|
|
291
|
+
--round:ui-rounded,"SF Pro Rounded",-apple-system,system-ui,sans-serif;}
|
|
292
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
293
|
+
body{font-family:var(--sans);background:
|
|
294
|
+
radial-gradient(1200px 600px at 80% -10%,#15303a 0,transparent 55%),
|
|
295
|
+
radial-gradient(900px 500px at -10% 10%,#172a38 0,transparent 50%),var(--ink);
|
|
296
|
+
color:var(--text);min-height:100vh;-webkit-font-smoothing:antialiased;line-height:1.5}
|
|
297
|
+
.wrap{max-width:900px;margin:0 auto;padding:22px 18px 60px}
|
|
298
|
+
header{display:flex;align-items:center;gap:12px;margin-bottom:26px}
|
|
299
|
+
.logo{display:flex;align-items:center;gap:10px;font-weight:700;letter-spacing:-.01em}
|
|
300
|
+
.logo .mk{width:30px;height:30px;border-radius:9px;background:linear-gradient(145deg,var(--good),#1b9e78);
|
|
301
|
+
display:grid;place-items:center;font-size:16px;box-shadow:0 4px 14px #36d39944}
|
|
302
|
+
.hactions{margin-left:auto;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
303
|
+
.btn{font:inherit;font-size:12.5px;color:var(--text);background:var(--surf);border:1px solid var(--line);
|
|
304
|
+
border-radius:10px;padding:7px 12px;cursor:pointer;transition:border-color .15s,background .15s}
|
|
305
|
+
.btn:hover{border-color:#3b5464;background:var(--surf2)}
|
|
306
|
+
.btn:focus-visible{outline:2px solid var(--good);outline-offset:2px}
|
|
307
|
+
.chip-local{color:var(--good);border-color:#2b6b55;background:#12302a}
|
|
308
|
+
/* hero */
|
|
309
|
+
.hero{display:flex;gap:28px;align-items:center;background:var(--surf);border:1px solid var(--line);
|
|
310
|
+
border-radius:var(--r);padding:26px 28px;position:relative;overflow:hidden}
|
|
311
|
+
.ringwrap{position:relative;width:150px;height:150px;flex:0 0 auto}
|
|
312
|
+
.ring{transform:rotate(-90deg)}
|
|
313
|
+
.ring circle{fill:none;stroke-width:11;stroke-linecap:round}
|
|
314
|
+
.ring .bg{stroke:#223642}
|
|
315
|
+
.ring .fg{transition:stroke-dashoffset 1.4s cubic-bezier(.22,1,.36,1);stroke:var(--good)}
|
|
316
|
+
.ringnum{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center}
|
|
317
|
+
.ringnum b{font-family:var(--round);font-size:52px;font-weight:800;line-height:1;letter-spacing:-.02em}
|
|
318
|
+
.ringnum span{font-size:11px;color:var(--muted);margin-top:3px}
|
|
319
|
+
.verdict{min-width:0}
|
|
320
|
+
.verdict .vtag{display:inline-block;font-size:13px;font-weight:700;padding:4px 12px;border-radius:999px;margin-bottom:10px}
|
|
321
|
+
.verdict h1{font-size:25px;font-weight:700;letter-spacing:-.02em;margin-bottom:6px}
|
|
322
|
+
.verdict p{color:var(--muted);font-size:13.5px;max-width:46ch}
|
|
323
|
+
.verdict .meta{color:var(--faint);font-size:12px;margin-top:12px}
|
|
324
|
+
/* vitals */
|
|
325
|
+
.sec-h{font-size:13px;color:var(--muted);margin:30px 2px 12px;font-weight:600}
|
|
326
|
+
.vitals{display:grid;grid-template-columns:repeat(5,1fr);gap:10px}
|
|
327
|
+
.vital{background:var(--surf);border:1px solid var(--line);border-radius:14px;padding:13px 13px 15px;cursor:pointer;
|
|
328
|
+
text-align:left;transition:transform .12s,border-color .15s;color:inherit;font:inherit}
|
|
329
|
+
.vital:hover{transform:translateY(-2px);border-color:#3b5464}
|
|
330
|
+
.vital[aria-pressed=true]{border-color:var(--good);box-shadow:0 0 0 1px var(--good) inset}
|
|
331
|
+
.vital .vn{font-size:12.5px;font-weight:600;margin-bottom:10px;display:flex;align-items:center;gap:6px}
|
|
332
|
+
.vital .dot{width:8px;height:8px;border-radius:50%}
|
|
333
|
+
.vital .bar{height:5px;border-radius:3px;background:#223642;overflow:hidden}
|
|
334
|
+
.vital .bar i{display:block;height:100%;border-radius:3px;width:0;transition:width 1s .3s ease}
|
|
335
|
+
.vital .vs{font-size:11px;color:var(--muted);margin-top:8px}
|
|
336
|
+
/* controls */
|
|
337
|
+
.controls{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:26px 2px 12px}
|
|
338
|
+
.fchip{font:inherit;font-size:12.5px;cursor:pointer;color:var(--muted);background:transparent;border:1px solid var(--line);
|
|
339
|
+
border-radius:999px;padding:6px 13px;transition:all .14s}
|
|
340
|
+
.fchip[aria-pressed=true]{color:var(--ink);background:var(--text);border-color:var(--text);font-weight:600}
|
|
341
|
+
.fcount{margin-left:auto;color:var(--faint);font-size:12.5px}
|
|
342
|
+
/* findings */
|
|
343
|
+
.find{background:var(--surf);border:1px solid var(--line);border-radius:14px;margin-bottom:9px;overflow:hidden}
|
|
344
|
+
.find>button{all:unset;display:flex;align-items:center;gap:11px;width:100%;padding:15px 16px;cursor:pointer;box-sizing:border-box}
|
|
345
|
+
.find>button:focus-visible{outline:2px solid var(--good);outline-offset:-2px}
|
|
346
|
+
.sdot{width:9px;height:9px;border-radius:50%;flex:0 0 auto}
|
|
347
|
+
.ftitle{font-size:14px;font-weight:600;flex:1;min-width:0}
|
|
348
|
+
.ftag{font-size:11px;color:var(--muted);background:#1f3340;padding:2px 8px;border-radius:6px;flex:0 0 auto}
|
|
349
|
+
.chev{color:var(--faint);transition:transform .2s;flex:0 0 auto}
|
|
350
|
+
.find.open .chev{transform:rotate(90deg)}
|
|
351
|
+
.body{max-height:0;overflow:hidden;transition:max-height .28s ease}
|
|
352
|
+
.body .inner{padding:0 16px 16px 36px}
|
|
353
|
+
.detail{color:var(--muted);font-size:13px;margin-bottom:12px}
|
|
354
|
+
.why{background:#13262f;border-left:3px solid var(--info);border-radius:0 10px 10px 0;padding:10px 13px;margin-bottom:12px}
|
|
355
|
+
.why .wl{font-size:11px;color:var(--info);font-weight:700;margin-bottom:4px}
|
|
356
|
+
.why p{font-size:12.5px;color:#c7d7df;line-height:1.55}
|
|
357
|
+
.fixbox{background:#102a22;border:1px solid #1e4a3a;border-radius:10px;padding:11px 13px;display:flex;gap:10px;align-items:flex-start}
|
|
358
|
+
.fixbox .fl{font-size:11px;color:var(--good);font-weight:700;margin-bottom:4px}
|
|
359
|
+
.fixbox p{font-size:12.5px;color:#cdeadf;line-height:1.5}
|
|
360
|
+
.copy{margin-left:auto;flex:0 0 auto}
|
|
361
|
+
.clean{background:var(--surf);border:1px solid #1e4a3a;border-radius:14px;padding:22px;text-align:center;color:var(--good);font-size:14px}
|
|
362
|
+
footer{margin-top:30px;color:var(--faint);font-size:11.5px;line-height:1.7;border-top:1px solid var(--line);padding-top:16px}
|
|
363
|
+
/* modal */
|
|
364
|
+
.modal{position:fixed;inset:0;background:#06101699;display:grid;place-items:center;padding:20px;z-index:50}
|
|
365
|
+
.modal[hidden]{display:none}
|
|
366
|
+
.modal .box{background:var(--surf);border:1px solid var(--line);border-radius:var(--r);padding:24px;max-width:440px}
|
|
367
|
+
.modal h3{font-size:16px;margin-bottom:10px;color:var(--good)}
|
|
368
|
+
.modal p{font-size:13px;color:var(--muted);line-height:1.6}
|
|
369
|
+
.modal .btn{margin-top:16px}
|
|
370
|
+
/* ์น์
2 ํผ๋ ๋คํธ์ํฌ (์ฝ์ด+5 ํผ๋๋
ธ๋ ํตํฉ) */
|
|
371
|
+
.netwrap{position:relative;width:100%;aspect-ratio:920/600;background:var(--surf);border:1px solid var(--line);border-radius:var(--r);overflow:hidden}
|
|
372
|
+
#netlines{position:absolute;inset:0;width:100%;height:100%}
|
|
373
|
+
.mon.fnode{position:absolute;transform:translate(-50%,-50%);width:22.5%;min-width:170px;z-index:2}
|
|
374
|
+
.mon.fnode:hover{transform:translate(-50%,calc(-50% - 2px))}
|
|
375
|
+
.fnode .feed{min-height:58px}
|
|
376
|
+
@media (max-width:900px){
|
|
377
|
+
.netwrap{aspect-ratio:auto;height:auto;display:grid;grid-template-columns:1fr 1fr;gap:9px;padding:12px}
|
|
378
|
+
#netlines{display:none}
|
|
379
|
+
.mon.fnode,.mon.fnode:hover{position:static;transform:none;width:auto;min-width:0}
|
|
380
|
+
.fnode .feed{min-height:70px}
|
|
381
|
+
}
|
|
382
|
+
/* ๋ณด์ ๋ชจ๋ ์นด๋ (Q-048) */
|
|
383
|
+
.cardgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:12px;margin-bottom:8px}
|
|
384
|
+
@media (min-width:2400px){.cardgrid{grid-template-columns:repeat(auto-fill,minmax(420px,1fr))}}
|
|
385
|
+
.mcard{background:var(--surf);border:1px solid var(--line);border-radius:var(--r);padding:16px 16px 14px;display:flex;flex-direction:column;gap:9px;min-width:0}
|
|
386
|
+
.mcard.alert{border-color:var(--crit);box-shadow:0 0 0 1px var(--crit) inset}
|
|
387
|
+
.mcard.stale{border-color:var(--warn)}
|
|
388
|
+
/* SEC-002 ๊ธ๋ก์ฐ: ์ค๊ฐ๋(์ง์ผ๋ณด๋ ์คยท์๋์ผ๋ก ๋ง๋ ์ค) ์นด๋๋ง ์์ํ pulse โ ์ค๋น ์คยท๊บผ์ ธ ์์์ ๋ฌด๊ด(๊ฑฐ์ง ์์ฌ ๋ฐฉ์ง) */
|
|
389
|
+
@keyframes livePulse{0%{box-shadow:0 0 6px 0 rgba(54,211,153,.10),0 0 0 1px rgba(54,211,153,.14) inset;border-color:rgba(54,211,153,.35)}100%{box-shadow:0 0 16px 2px rgba(54,211,153,.26),0 0 0 1px rgba(54,211,153,.30) inset;border-color:rgba(54,211,153,.60)}}
|
|
390
|
+
@keyframes livePulseAmber{0%{box-shadow:0 0 6px 0 rgba(255,159,67,.10),0 0 0 1px rgba(255,159,67,.14) inset;border-color:rgba(255,159,67,.35)}100%{box-shadow:0 0 16px 2px rgba(255,159,67,.28),0 0 0 1px rgba(255,159,67,.32) inset;border-color:rgba(255,159,67,.60)}}
|
|
391
|
+
.mcard.live{animation:livePulse 4s ease-in-out infinite alternate}
|
|
392
|
+
.mcard.live.stale{animation:livePulseAmber 4s ease-in-out infinite alternate}
|
|
393
|
+
.mcard.live.alert{animation:none}
|
|
394
|
+
@media (prefers-reduced-motion: reduce){.mcard.live,.mcard.live.stale{animation:none;box-shadow:0 0 10px 1px rgba(54,211,153,.18);border-color:rgba(54,211,153,.45)}}
|
|
395
|
+
.mcard .mh2{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
396
|
+
.mcard .mh2 b{font-size:15px;overflow-wrap:anywhere}
|
|
397
|
+
.cbadge{display:inline-block;padding:2px 9px;border-radius:999px;font-size:11.5px;font-weight:800;background:#ffffff14;color:var(--muted);white-space:normal}
|
|
398
|
+
.cbadge.b-warn{background:#f5b94a22;color:var(--warn)}.cbadge.b-block{background:#36d39922;color:var(--good)}
|
|
399
|
+
.cbadge.b-off{background:#ffffff10;color:var(--muted)}.cbadge.b-plan{background:#6aa8ff22;color:var(--info)}
|
|
400
|
+
.cbadge.b-stale{background:#f5b94a33;color:var(--warn)}.cbadge.b-broken{background:#ff5d6c33;color:var(--crit)}
|
|
401
|
+
.cbadge.b-ask{background:#ff9f4326;color:#ff9f43}
|
|
402
|
+
.mcard .fld{font-size:12.5px;line-height:1.55;color:var(--muted);overflow-wrap:anywhere}
|
|
403
|
+
.mcard .fld i{font-style:normal;font-weight:800;color:var(--text);margin-right:6px}
|
|
404
|
+
.mcard .fld.lim{color:#e7c987}
|
|
405
|
+
.mcard .act{font-size:13px;color:var(--text);line-height:1.6;overflow-wrap:anywhere}
|
|
406
|
+
.kv{display:flex;flex-wrap:wrap;gap:6px}.kv span{background:#ffffff0d;border-radius:8px;padding:3px 9px;font-size:12px;white-space:normal}
|
|
407
|
+
.kv span b{margin-left:4px}.kv .g-now b{color:var(--crit)}.kv .g-pri b{color:#FF9F45}
|
|
408
|
+
.mcard .more{align-self:flex-start;cursor:pointer}
|
|
409
|
+
.mcard .moreb{display:none;font-size:12.5px;line-height:1.6;color:var(--muted);border-top:1px solid var(--line);padding-top:8px;overflow-wrap:anywhere}
|
|
410
|
+
.mcard.open .moreb{display:block}
|
|
411
|
+
.mcard .moreb div{padding:2px 0}
|
|
412
|
+
/* ๋ธ๋ญ ๋ฒํธ ๋ฐฐ์ง */
|
|
413
|
+
.secbadge{display:inline-flex;align-items:center;justify-content:center;min-width:22px;height:22px;padding:0 7px;border-radius:7px;background:var(--good);color:#06181f;font-weight:800;font-size:12px;margin-right:9px;vertical-align:middle}
|
|
414
|
+
.anum{display:inline-flex;align-items:center;justify-content:center;width:19px;height:19px;border-radius:50%;border:1.5px solid var(--good);color:var(--good);font-weight:800;font-size:11px;flex:0 0 auto}
|
|
415
|
+
.herobadge{position:absolute;top:12px;left:14px}
|
|
416
|
+
/* live scan monitors */
|
|
417
|
+
.monitors{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:11px}
|
|
418
|
+
.mon{background:var(--surf);border:1px solid var(--line);border-radius:14px;padding:13px 14px 11px;cursor:pointer;
|
|
419
|
+
position:relative;overflow:hidden;transition:border-color .15s,transform .12s}
|
|
420
|
+
.mon:hover{border-color:#3b5464;transform:translateY(-2px)}
|
|
421
|
+
.mon[aria-pressed=true]{border-color:var(--good);box-shadow:0 0 0 1px var(--good) inset}
|
|
422
|
+
.mon .mininet{position:absolute;inset:0;z-index:0;opacity:.32;pointer-events:none}
|
|
423
|
+
.mon .mh,.mon .feed{position:relative;z-index:1}
|
|
424
|
+
.mon .mh{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;margin-bottom:9px}
|
|
425
|
+
.mon .mh .mi{width:20px;height:20px;flex:0 0 auto}
|
|
426
|
+
.mon .mh .ms{margin-left:auto;font-size:9.5px;color:var(--good);display:flex;align-items:center;gap:4px}
|
|
427
|
+
.mon .mh .ms .d{width:6px;height:6px;border-radius:50%;background:var(--good);animation:pd 1.5s infinite}
|
|
428
|
+
.mon .scanline{position:absolute;left:0;right:0;height:28px;top:34px;
|
|
429
|
+
background:linear-gradient(90deg,transparent,#36d39911 40%,#36d39922 50%,#36d39911 60%,transparent);
|
|
430
|
+
animation:sweep 2.4s linear infinite;pointer-events:none}
|
|
431
|
+
@keyframes sweep{0%{transform:translateY(-6px);opacity:0}20%{opacity:1}80%{opacity:1}100%{transform:translateY(96px);opacity:0}}
|
|
432
|
+
.feed{font-family:ui-monospace,"SF Mono",Menlo,monospace;font-size:11px;line-height:1.75;min-height:90px}
|
|
433
|
+
.feed .ln{display:flex;gap:7px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
434
|
+
.feed .ln .ic{flex:0 0 auto}
|
|
435
|
+
.feed .ln.scan .ic{color:var(--warn)}
|
|
436
|
+
.feed .ln.ok{color:#9fb6c0}.feed .ln.ok .ic{color:var(--good)}
|
|
437
|
+
.feed .cur{color:var(--good)}
|
|
438
|
+
.feed .cur b{animation:blink 1s steps(1) infinite}
|
|
439
|
+
@keyframes blink{50%{opacity:0}}
|
|
440
|
+
/* live monitoring */
|
|
441
|
+
.livebar{display:flex;align-items:center;gap:9px;font-size:12.5px;color:var(--good);margin:0 2px 16px;font-weight:600}
|
|
442
|
+
.pulse-dot{width:9px;height:9px;border-radius:50%;background:var(--good);animation:pd 1.7s infinite}
|
|
443
|
+
@keyframes pd{0%{box-shadow:0 0 0 0 #36d39988}70%{box-shadow:0 0 0 8px #36d39900}100%{box-shadow:0 0 0 0 #36d39900}}
|
|
444
|
+
.clock{margin-left:auto;color:var(--muted);font-variant-numeric:tabular-nums;font-weight:500;letter-spacing:.02em}
|
|
445
|
+
.net{background:var(--surf);border:1px solid var(--line);border-radius:var(--r);padding:12px 10px 8px}
|
|
446
|
+
.net svg{width:100%;height:auto;display:block}
|
|
447
|
+
.nnode{cursor:pointer}
|
|
448
|
+
.nnode text{font-family:var(--sans)}
|
|
449
|
+
.nnode:focus-visible{outline:none}
|
|
450
|
+
.nnode:focus-visible .nbox{stroke:var(--good);stroke-width:2}
|
|
451
|
+
.nnode[aria-pressed=true] .nbox{stroke:var(--good);stroke-width:2.5}
|
|
452
|
+
.ring-glow{animation:breathe 3.2s ease-in-out infinite;transform-origin:center}
|
|
453
|
+
@keyframes breathe{0%,100%{opacity:.14;r:40}50%{opacity:.3;r:46}}
|
|
454
|
+
.ringwrap::after{content:"";position:absolute;inset:-6px;border-radius:50%;box-shadow:0 0 34px 2px #36d39933;animation:breathe2 3.2s ease-in-out infinite;pointer-events:none}
|
|
455
|
+
@keyframes breathe2{0%,100%{opacity:.3}50%{opacity:.7}}
|
|
456
|
+
@media (max-width:620px){.hero{flex-direction:column;text-align:center}.verdict p{max-width:none}
|
|
457
|
+
.wrap{padding:16px 14px 48px}}
|
|
458
|
+
@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}
|
|
459
|
+
</style></head>
|
|
460
|
+
<body><div class="wrap">
|
|
461
|
+
<header>
|
|
462
|
+
<div class="logo"><span class="mk">๐ก๏ธ</span><span id="brand"></span></div>
|
|
463
|
+
<div class="hactions">
|
|
464
|
+
<button class="btn chip-local" id="localBadge"></button>
|
|
465
|
+
<button class="btn" id="exportBtn"></button>
|
|
466
|
+
<button class="btn" id="printBtn"></button>
|
|
467
|
+
</div>
|
|
468
|
+
</header>
|
|
469
|
+
<div class="livebar"><span class="pulse-dot"></span><span id="liveTxt"></span><span class="clock" id="clock"></span></div>
|
|
470
|
+
<section class="hero">
|
|
471
|
+
<span class="secbadge herobadge">1</span>
|
|
472
|
+
<div class="ringwrap"><svg class="ring" width="150" height="150" viewBox="0 0 120 120">
|
|
473
|
+
<circle class="bg" cx="60" cy="60" r="52"></circle>
|
|
474
|
+
<circle class="fg" cx="60" cy="60" r="52"></circle></svg>
|
|
475
|
+
<div class="ringnum"><b id="scoreNum">0</b><span id="scoreLab"></span></div></div>
|
|
476
|
+
<div class="verdict">
|
|
477
|
+
<span class="vtag" id="vtag"></span>
|
|
478
|
+
<h1 id="verdictTitle"></h1>
|
|
479
|
+
<p id="whatScore"></p>
|
|
480
|
+
<div class="meta" id="scanMeta"></div>
|
|
481
|
+
</div>
|
|
482
|
+
</section>
|
|
483
|
+
<div class="sec-h" id="vitalsH"></div>
|
|
484
|
+
<div class="netwrap" id="netwrap"><svg id="netlines" viewBox="0 0 920 600" preserveAspectRatio="xMidYMid meet"></svg></div>
|
|
485
|
+
<div class="sec-h" id="findH"></div>
|
|
486
|
+
<div class="controls" id="filters"></div>
|
|
487
|
+
<div id="findings"></div>
|
|
488
|
+
<div class="sec-h" id="cardsH" hidden></div>
|
|
489
|
+
<div class="cardgrid" id="cardgrid"></div>
|
|
490
|
+
<footer id="foot"></footer>
|
|
491
|
+
</div>
|
|
492
|
+
<div class="modal" id="modal" hidden><div class="box">
|
|
493
|
+
<h3 id="mTitle"></h3><p id="mBody"></p>
|
|
494
|
+
<button class="btn" id="mClose">OK</button></div></div>
|
|
495
|
+
<script>
|
|
496
|
+
const DATA = /*DATA*/;
|
|
497
|
+
const UI = /*UI*/;
|
|
498
|
+
const CARDS = /*CARDS*/;
|
|
499
|
+
const CHECKS = /*CHECKS*/;
|
|
500
|
+
const AREAS=['cred','net','os','ai','backup'];
|
|
501
|
+
const AREA_ICON={cred:'๐',net:'๐',os:'๐ป',ai:'๐ค',backup:'๐พ'};
|
|
502
|
+
const AREA_NAME=UI.areas;
|
|
503
|
+
const SEVC={HIGH:'var(--crit)',MED:'var(--warn)',INFO:'var(--info)'};
|
|
504
|
+
const scoreColor=s=>s>=90?'var(--good)':s>=70?'var(--warn)':s>=40?'#FF9F45':'var(--crit)';
|
|
505
|
+
let fSev='all',fArea=null;
|
|
506
|
+
|
|
507
|
+
document.getElementById('brand').textContent=UI.brand;
|
|
508
|
+
document.getElementById('scoreLab').textContent=UI.immunity;
|
|
509
|
+
document.getElementById('whatScore').textContent=UI.whatscore;
|
|
510
|
+
document.getElementById('scanMeta').textContent=UI.scanned+' ยท '+DATA.ts;
|
|
511
|
+
document.getElementById('vitalsH').innerHTML='<span class="secbadge">2</span>'+UI.net_title;
|
|
512
|
+
document.getElementById('findH').innerHTML='<span class="secbadge">3</span>'+UI.sec_find;
|
|
513
|
+
document.getElementById('liveTxt').textContent=UI.live_watch;
|
|
514
|
+
(function(){function tick(){document.getElementById('clock').textContent=new Date().toLocaleTimeString('__L__'==='ko'?'ko-KR':'en-US');}tick();setInterval(tick,1000);})();
|
|
515
|
+
document.getElementById('localBadge').textContent='๐ '+UI.local;
|
|
516
|
+
document.getElementById('exportBtn').textContent='โ '+UI.export;
|
|
517
|
+
document.getElementById('printBtn').textContent='๐จ '+UI.print;
|
|
518
|
+
document.getElementById('foot').textContent=UI.local_more;
|
|
519
|
+
|
|
520
|
+
// verdict
|
|
521
|
+
const sc=DATA.score;
|
|
522
|
+
const vt=document.getElementById('vtag');
|
|
523
|
+
let vlabel=sc>=90?UI.v_good:sc>=60?UI.v_warn:UI.v_crit;
|
|
524
|
+
vt.textContent=vlabel;vt.style.background=scoreColor(sc)+'22';vt.style.color=scoreColor(sc);
|
|
525
|
+
document.getElementById('verdictTitle').textContent=(DATA.user||'')+UI.hero_sub;
|
|
526
|
+
|
|
527
|
+
// ring + count-up
|
|
528
|
+
const C=2*Math.PI*52, fg=document.querySelector('.ring .fg');
|
|
529
|
+
fg.style.strokeDasharray=C; fg.style.strokeDashoffset=C; fg.style.stroke=scoreColor(sc);
|
|
530
|
+
const reduce=matchMedia('(prefers-reduced-motion:reduce)').matches;
|
|
531
|
+
requestAnimationFrame(()=>{setTimeout(()=>{fg.style.strokeDashoffset=C*(1-sc/100);},120)});
|
|
532
|
+
const numEl=document.getElementById('scoreNum');
|
|
533
|
+
if(reduce){numEl.textContent=sc;}else{let n=0;const step=Math.max(1,Math.round(sc/40));
|
|
534
|
+
const t=setInterval(()=>{n+=step;if(n>=sc){n=sc;clearInterval(t);}numEl.textContent=n;},26);}
|
|
535
|
+
numEl.style.color=scoreColor(sc);
|
|
536
|
+
|
|
537
|
+
// vitals
|
|
538
|
+
function areaStat(a){const fs=DATA.findings.filter(f=>f.areaKey===a);
|
|
539
|
+
if(fs.some(f=>f.sev==='HIGH'))return{c:'var(--crit)',w:34,n:fs.filter(f=>f.sev!=='INFO').length};
|
|
540
|
+
if(fs.some(f=>f.sev==='MED'))return{c:'var(--warn)',w:66,n:fs.filter(f=>f.sev!=='INFO').length};
|
|
541
|
+
return{c:'var(--good)',w:100,n:0};}
|
|
542
|
+
// ์ธ๋ จ๋ ๋ผ์ธ ์์ด์ฝ (SVGยท์ด๋ชจ์ง ๋์ฒด)
|
|
543
|
+
const ICONS={
|
|
544
|
+
core:'<path d="M12 2.5l7.5 3.2v5.1c0 4.8-3.2 8.6-7.5 10.7-4.3-2.1-7.5-5.9-7.5-10.7V5.7z"/><path d="M8.6 12.2l2.3 2.3 4.5-4.6"/>',
|
|
545
|
+
cred:'<circle cx="8" cy="8" r="3.4"/><path d="M10.4 10.4L19 19M15.8 17.8l2.2 2.2M18.3 15.3l2.2 2.2"/>',
|
|
546
|
+
net:'<circle cx="12" cy="12" r="8.3"/><path d="M3.7 12h16.6M12 3.7c-3.2 4.2-3.2 12.4 0 16.6M12 3.7c3.2 4.2 3.2 12.4 0 16.6"/>',
|
|
547
|
+
os:'<rect x="3" y="4.6" width="18" height="12" rx="1.8"/><path d="M8.8 20.6h6.4M12 16.6v4"/>',
|
|
548
|
+
ai:'<rect x="6.6" y="6.6" width="10.8" height="10.8" rx="2.4"/><circle cx="12" cy="12" r="2.2"/><path d="M12 2.6v4M12 17.4v4M2.6 12h4M17.4 12h4"/>',
|
|
549
|
+
backup:'<ellipse cx="12" cy="6" rx="7" ry="2.9"/><path d="M5 6v12c0 1.6 3.1 2.9 7 2.9s7-1.3 7-2.9V6M5 12c0 1.6 3.1 2.9 7 2.9s7-1.3 7-2.9"/>'
|
|
550
|
+
};
|
|
551
|
+
function svgIcon(name,x,y,color,s){s=s||.72;
|
|
552
|
+
return '<g transform="translate('+x+','+y+') scale('+s+')" fill="none" stroke="'+color+'" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">'+ICONS[name]+'</g>';}
|
|
553
|
+
|
|
554
|
+
// ๋ธ๋ญ ์์์ ์์ง์ด๋ ๋ฏธ๋ ์ ๊ฒฝ๋ง (์์ญ๋ณ ๋ณ์ฃผ)
|
|
555
|
+
function miniNet(col,idx){
|
|
556
|
+
const base=[[38,28],[118,66],[198,34],[158,96],[70,92]];
|
|
557
|
+
const pts=base.map((p,k)=>[p[0]+((idx*13+k*7)%22)-11, p[1]+((idx*9+k*5)%18)-9]);
|
|
558
|
+
let lines='',path='M',nodes='';
|
|
559
|
+
pts.forEach((p,k)=>{path+=(k?' L':'')+p[0].toFixed(0)+','+p[1].toFixed(0);
|
|
560
|
+
nodes+='<circle cx="'+p[0].toFixed(0)+'" cy="'+p[1].toFixed(0)+'" r="2.4" fill="'+col+'"/>';});
|
|
561
|
+
for(let k=0;k<pts.length-1;k++){lines+='<line x1="'+pts[k][0].toFixed(0)+'" y1="'+pts[k][1].toFixed(0)+'" x2="'+pts[k+1][0].toFixed(0)+'" y2="'+pts[k+1][1].toFixed(0)+'" stroke="'+col+'" stroke-opacity=".35" stroke-width="1"/>';}
|
|
562
|
+
const pulse=(beg,r)=>'<circle r="'+r+'" fill="'+col+'" filter="url(#g)"><animateMotion dur="'+(3+idx*0.35).toFixed(1)+'s" begin="'+beg+'s" repeatCount="indefinite" calcMode="spline" keyPoints="0;1" keyTimes="0;1" keySplines="0.4 0 0.6 1" path="'+path+'"/></circle>';
|
|
563
|
+
return '<svg class="mininet" viewBox="0 0 260 120" preserveAspectRatio="xMidYMid slice">'+lines+nodes+pulse('0',2.8)+pulse((1.2+idx*0.2).toFixed(1),2.1)+'</svg>';
|
|
564
|
+
}
|
|
565
|
+
function pad(n){return(n<10?'0':'')+n;}
|
|
566
|
+
function nowt(){const d=new Date();return pad(d.getHours())+':'+pad(d.getMinutes())+':'+pad(d.getSeconds());}
|
|
567
|
+
// ์น์
2 ํตํฉ: ์ฝ์ด + 5 ํผ๋๋
ธ๋(์ค์บ๋ชจ๋ํฐ) + ์ ๊ฒฝ๋ง ํ์ค
|
|
568
|
+
function buildNetwork(){
|
|
569
|
+
const cx=460,cy=300,pts={cred:[460,74],net:[678,232],os:[596,486],ai:[324,486],backup:[242,232]};
|
|
570
|
+
const overall=DATA.high?'var(--crit)':DATA.med?'var(--warn)':'var(--good)';
|
|
571
|
+
let paths='',flows='',pulses='';
|
|
572
|
+
const mk=(pd,dur,beg,r,col)=>'<circle r="'+r+'" fill="'+col+'" filter="url(#g)"><animateMotion dur="'+dur+'s" begin="'+beg+'s" repeatCount="indefinite" calcMode="spline" keyPoints="0;1" keyTimes="0;1" keySplines="0.4 0 0.6 1" path="'+pd+'"/></circle>';
|
|
573
|
+
AREAS.forEach((a,i)=>{const p=pts[a],x=p[0],y=p[1],s=areaStat(a);
|
|
574
|
+
const mx=(cx+x)/2,my=(cy+y)/2,dx=x-cx,dy=y-cy,ln=Math.hypot(dx,dy)||1,off=46*(i%2?1:-1);
|
|
575
|
+
const ctx=(mx+(-dy/ln)*off).toFixed(0),cty=(my+(dx/ln)*off).toFixed(0);
|
|
576
|
+
const d='M'+cx+','+cy+' Q'+ctx+','+cty+' '+x+','+y, dr='M'+x+','+y+' Q'+ctx+','+cty+' '+cx+','+cy;
|
|
577
|
+
paths+='<path d="'+d+'" fill="none" stroke="'+s.c+'" stroke-opacity=".28" stroke-width="1.6"/>';
|
|
578
|
+
flows+='<path d="'+d+'" fill="none" stroke="'+s.c+'" stroke-opacity=".65" stroke-width="1.8" stroke-linecap="round" stroke-dasharray="4 16"><animate attributeName="stroke-dashoffset" values="0;-20" dur="'+(1.2+i*0.12).toFixed(2)+'s" repeatCount="indefinite"/></path>';
|
|
579
|
+
pulses+=mk(d,(2.1+i*0.15).toFixed(2),(i*0.3).toFixed(2),4.2,s.c)+mk(dr,(2.5+i*0.1).toFixed(2),(i*0.3+0.9).toFixed(2),3,'#eaf1f4')+mk(d,'2.9',(i*0.3+1.6).toFixed(2),2.6,s.c);
|
|
580
|
+
});
|
|
581
|
+
const radar=(beg)=>'<circle cx="'+cx+'" cy="'+cy+'" r="48" fill="none" stroke="'+overall+'" stroke-width="2"><animate attributeName="r" values="48;92" dur="3s" begin="'+beg+'s" repeatCount="indefinite"/><animate attributeName="opacity" values="0.5;0" dur="3s" begin="'+beg+'s" repeatCount="indefinite"/></circle>';
|
|
582
|
+
const core='<g>'+radar('0')+radar('1.5')+'<circle class="ring-glow" cx="'+cx+'" cy="'+cy+'" r="64" fill="'+overall+'"/><circle cx="'+cx+'" cy="'+cy+'" r="48" fill="var(--surf2)" stroke="'+overall+'" stroke-width="2.5"/>'+svgIcon('core',cx-17,cy-26,overall,1.5)+'<text x="'+cx+'" y="'+(cy+32)+'" text-anchor="middle" font-size="12" fill="var(--muted)">'+UI.watching+'</text></g>';
|
|
583
|
+
document.getElementById('netlines').innerHTML='<defs><filter id="g" x="-60%" y="-60%" width="220%" height="220%"><feGaussianBlur stdDeviation="2.6"/></filter></defs>'+paths+flows+core+pulses;
|
|
584
|
+
const wrap=document.getElementById('netwrap');
|
|
585
|
+
AREAS.forEach((a,idx)=>{const p=pts[a],s=areaStat(a),nm=AREA_NAME[a]||a;
|
|
586
|
+
const card=document.createElement('div');card.className='mon fnode';card.dataset.area=a;
|
|
587
|
+
card.style.left=(p[0]/920*100).toFixed(2)+'%';card.style.top=(p[1]/600*100).toFixed(2)+'%';
|
|
588
|
+
card.setAttribute('role','button');card.setAttribute('tabindex','0');card.setAttribute('aria-pressed','false');
|
|
589
|
+
card.innerHTML=(reduce?'':miniNet(s.c,idx))+
|
|
590
|
+
'<div class="mh"><span class="anum">'+(idx+1)+'</span><svg class="mi" viewBox="0 0 24 24" fill="none" stroke="'+s.c+'" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">'+ICONS[a]+'</svg><span>'+nm+'</span><span class="ms"><span class="d"></span>'+UI.scanning+'</span></div>'+
|
|
591
|
+
(reduce?'':'<div class="scanline"></div>')+
|
|
592
|
+
'<div class="feed" id="feed-'+a+'"></div>';
|
|
593
|
+
const act=()=>{fArea=(fArea===a)?null:a;document.querySelectorAll('.fnode').forEach(m=>m.setAttribute('aria-pressed',m.dataset.area===fArea));render();};
|
|
594
|
+
card.onclick=act;card.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();act();}});
|
|
595
|
+
wrap.appendChild(card);
|
|
596
|
+
runFeed(a,CHECKS[a]||[],s.c,idx);
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
function shuffle(arr){for(let k=arr.length-1;k>0;k--){const j=Math.floor(Math.random()*(k+1));const t=arr[k];arr[k]=arr[j];arr[j]=t;}return arr;}
|
|
600
|
+
function runFeed(a,steps0,col,idx){
|
|
601
|
+
const feed=document.getElementById('feed-'+a);if(!feed||!steps0.length)return;
|
|
602
|
+
const MAX=4;let done=[];let order=shuffle(steps0.slice());let i=0;
|
|
603
|
+
function paint(scanIdx){
|
|
604
|
+
let h='';
|
|
605
|
+
done.slice(-MAX+1).forEach(t=>{h+='<div class="ln ok"><span class="ic">โ</span><span>'+t.s+'</span><span style="margin-left:auto;opacity:.5">'+t.t+'</span></div>';});
|
|
606
|
+
if(scanIdx!=null){h+='<div class="ln scan"><span class="ic">โณ</span><span>'+order[scanIdx]+'โฆ</span></div>';}
|
|
607
|
+
h+='<div class="ln cur"><span class="ic">โธ</span><b>_</b></div>';
|
|
608
|
+
feed.innerHTML=h;
|
|
609
|
+
}
|
|
610
|
+
if(reduce){done=steps0.map(s=>({s:s,t:nowt()}));paint(null);return;}
|
|
611
|
+
function cycle(){
|
|
612
|
+
paint(i);
|
|
613
|
+
const dwell=820+Math.floor(Math.random()*500); // ์ ๊ฒ๋ง๋ค ์์ ๋ฌ๋ผ ๋ ์๋๊ฐ
|
|
614
|
+
setTimeout(()=>{done.push({s:order[i],t:nowt()});i++;
|
|
615
|
+
if(i>=order.length){i=0;order=shuffle(steps0.slice());done=[];} // ๋งค ์ํ ์
ํ โ ๋ด์ฉ์ด ๊ณ์ ๋ฐ๋
|
|
616
|
+
setTimeout(cycle,300);},dwell);
|
|
617
|
+
}
|
|
618
|
+
setTimeout(cycle,idx*450);
|
|
619
|
+
}
|
|
620
|
+
buildNetwork();
|
|
621
|
+
if(reduce){document.querySelectorAll('animateMotion,animate').forEach(a=>a.remove());}
|
|
622
|
+
|
|
623
|
+
// filters
|
|
624
|
+
const counts={all:DATA.findings.length,HIGH:DATA.high,MED:DATA.med,INFO:DATA.findings.filter(f=>f.sev==='INFO').length};
|
|
625
|
+
const fcon=document.getElementById('filters');
|
|
626
|
+
[['all',UI.all,counts.all],['HIGH',UI.crit,counts.HIGH],['MED',UI.warn,counts.MED],['INFO',UI.info,counts.INFO]].forEach(([k,lab,n])=>{
|
|
627
|
+
const c=document.createElement('button');c.className='fchip';c.dataset.k=k;
|
|
628
|
+
c.setAttribute('aria-pressed',k==='all');c.textContent=lab+' '+n;
|
|
629
|
+
c.onclick=()=>{fSev=k;document.querySelectorAll('.fchip').forEach(x=>x.setAttribute('aria-pressed',x.dataset.k===k));render();};
|
|
630
|
+
fcon.appendChild(c);});
|
|
631
|
+
const fcount=document.createElement('span');fcount.className='fcount';fcon.appendChild(fcount);
|
|
632
|
+
|
|
633
|
+
// findings
|
|
634
|
+
const ORDER={HIGH:0,MED:1,INFO:2};
|
|
635
|
+
function render(){
|
|
636
|
+
let list=DATA.findings.slice().sort((a,b)=>ORDER[a.sev]-ORDER[b.sev]);
|
|
637
|
+
if(fSev!=='all')list=list.filter(f=>f.sev===fSev);
|
|
638
|
+
if(fArea)list=list.filter(f=>f.areaKey===fArea);
|
|
639
|
+
fcount.textContent=list.length+' '+UI.findings;
|
|
640
|
+
const con=document.getElementById('findings');con.innerHTML='';
|
|
641
|
+
if(!list.length){con.innerHTML='<div class="clean">'+UI.clean+'</div>';return;}
|
|
642
|
+
list.forEach(f=>{
|
|
643
|
+
const el=document.createElement('div');el.className='find';
|
|
644
|
+
const sev=f.sev, col=SEVC[sev];
|
|
645
|
+
el.innerHTML=
|
|
646
|
+
'<button aria-expanded="false"><span class="sdot" style="background:'+col+'"></span>'+
|
|
647
|
+
'<span class="ftitle">'+esc(f.title)+'</span><span class="ftag">'+esc(f.area)+'</span>'+
|
|
648
|
+
'<span class="chev">โบ</span></button>'+
|
|
649
|
+
'<div class="body"><div class="inner">'+
|
|
650
|
+
'<div class="detail">'+esc(f.detail)+'</div>'+
|
|
651
|
+
'<div class="why"><div class="wl">๐ก '+UI.why+'</div><p>'+esc(f.why)+'</p></div>'+
|
|
652
|
+
'<div class="fixbox"><div><div class="fl">๐ง '+UI.fix+'</div><p class="fixtext">'+esc(f.fix)+'</p></div>'+
|
|
653
|
+
'<button class="btn copy">'+UI.copy+'</button></div>'+
|
|
654
|
+
'</div></div>';
|
|
655
|
+
const btn=el.querySelector('button[aria-expanded]'),body=el.querySelector('.body');
|
|
656
|
+
btn.onclick=()=>{const o=el.classList.toggle('open');btn.setAttribute('aria-expanded',o);
|
|
657
|
+
body.style.maxHeight=o?body.scrollHeight+'px':'0';};
|
|
658
|
+
el.querySelector('.copy').onclick=(e)=>{e.stopPropagation();
|
|
659
|
+
navigator.clipboard&&navigator.clipboard.writeText(f.fix);
|
|
660
|
+
e.target.textContent=UI.copied;setTimeout(()=>e.target.textContent=UI.copy,1400);};
|
|
661
|
+
con.appendChild(el);
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
function esc(s){return(s+'').replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c]))}
|
|
665
|
+
render();
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
// โโ ๋ณด์ ๋ชจ๋ ์นด๋ (security_cards.py ์ค๋ฐ์ดํฐยท์ฝ๊ธฐ ์ ์ฉ) โโ
|
|
669
|
+
(function(){
|
|
670
|
+
if(!CARDS||!CARDS.cards)return;
|
|
671
|
+
const H=document.getElementById('cardsH'),G=document.getElementById('cardgrid');
|
|
672
|
+
H.hidden=false;H.innerHTML='<span class="secbadge">4</span>๋ณด์ ๋ชจ๋ ์นด๋ <span style="font-size:12px;color:var(--muted);font-weight:400;margin-left:8px">์์ฑ '+esc(CARDS.generated)+' ยท ๊ฐ์ ๋ชจ๋ ๋ก๊ทธยทํผ๋์์ ๊ณ์ฐ</span>';
|
|
673
|
+
const BC={'์ง์ผ๋ณด๋ ์ค':'b-warn','์๋์ผ๋ก ๋ง๋ ์ค':'b-block','๊บผ์ ธ ์์':'b-off','์ค๋น ์ค':'b-plan','๋ด ํ์ธ ํ์':'b-ask','์ต์ ์ ๋ณด ์ฐ๊ฒฐ์ด ๋๊ฒผ์ด์':'b-broken',
|
|
674
|
+
'๊ฒฝ๊ณ ๋ชจ๋':'b-warn','์ฐจ๋จ๋ชจ๋':'b-block','๋นํ์ฑ':'b-off','์ค๊ณ':'b-plan','์ค๋๋จ':'b-stale','ํผ๋๋๊น':'b-broken'};
|
|
675
|
+
const bcls=s=>s&&s.indexOf('์ ๋ณด๊ฐ ์ค๋๋์ด์')===0?'b-stale':(BC[s]||'');
|
|
676
|
+
const nv=v=>(v===null||v===undefined)?'๋ฐ์ดํฐ ์์':v;
|
|
677
|
+
const kv=(pairs)=>'<div class="kv">'+pairs.map(p=>'<span'+(p[2]?' class="'+p[2]+'"':'')+'>'+esc(p[0])+'<b>'+esc(nv(p[1]))+'</b></span>').join('')+'</div>';
|
|
678
|
+
function actual(c){const a=c.actual;
|
|
679
|
+
if(c.card==='threat_intel'){
|
|
680
|
+
let h='<div class="act">'+esc(c.headline)+'</div><div class="act">'+esc(c.scope_line)+'</div>';
|
|
681
|
+
h+=kv([['OSV',a.records.osv],['KEV',a.records.kev],['EPSS',a.records.epss],['๊ฐฑ์ ์ฐ์์คํจ',a.update_fail_streak],['๋ง์ง๋ง ์ฑ๊ณต',a.last_update_ok]]);
|
|
682
|
+
if(a.triage)h+=kv([['์ฆ์',a.triage['์ฆ์'],'g-now'],['์ฐ์ ',a.triage['์ฐ์ '],'g-pri'],['๊ณํ',a.triage['๊ณํ']],['๋ชจ๋ํฐ๋ง',a.triage['๋ชจ๋ํฐ๋ง']],['KEV ๋งค์นญ',a.kev_matched],['์ด์ ๋๋น ์ ๊ท',a.new_since_yesterday]]);
|
|
683
|
+
else h+='<div class="act" style="color:var(--crit)">'+esc(a.not_scanned_reason||'๋ฑ๊ธ ํ์ ๋ถ๊ฐ')+'</div>';
|
|
684
|
+
if(a.not_scanned_reason&&a.triage)h+='<div class="act" style="color:var(--warn)">'+esc(a.not_scanned_reason)+'</div>';
|
|
685
|
+
return h;}
|
|
686
|
+
if(c.card==='agent_injection'){
|
|
687
|
+
const d=a.detected,f=a.false_positive;
|
|
688
|
+
let h='<div class="act">'+esc(c.note)+'</div>'+kv([['์ต๊ทผ '+a.window_days+'์ผ ํ์ง ๋ฎ์/์ค๊ฐ/๋์',d.low+' / '+d.mid+' / '+d.high],['shadow hold ํด๋น',a.shadow_hold.would_hold],['์๊ด ๊ฒฝ๋ณด',a.shadow_hold.correlation_alerts],['์นด๋๋ฆฌ ๋
ธ์ถ',a.canary_leaks],['์ก์ถ์ฑ ๋๊ตฌ ํธ์ถ ๊ธฐ๋ก',a.egress_tool_calls_logged],['holdยทaskยทdeny',a.holds===null?'๋ฏธ๊ตฌํ(์ค๋น ์ค)':a.holds]]);
|
|
689
|
+
h+=kv([['์คํ ๋ผ๋ฒจ๋จ',f.labeled],['๋ผ๋ฒจ ๋๊ธฐ',f.unlabeled],['์คํ๋ฅ ',f.rate_pct===null?'๋ผ๋ฒจ ์์ด ๋ฏธํ์':f.rate_pct+'%']]);
|
|
690
|
+
return h;}
|
|
691
|
+
if(c.card==='traceback'&&!a.data){
|
|
692
|
+
let h='<div class="act">'+esc(a.scan_ts?'์ต๊ทผ ์์ง '+a.scan_ts:'')+'</div>';
|
|
693
|
+
h+=kv([['ํ์ฑ ์๊ฒฉ ์ฐ๊ฒฐ',a.active_remote_conn],['๋ค์ด์จ ์ฐ๊ฒฐ',a.inbound_conn],['์๊ฒฉ ๋ก๊ทธ์ธ',a.remote_logins],['๋ก์ปฌ ๋ก๊ทธ์ธ',a.local_logins]]);
|
|
694
|
+
h+=kv([['์๊ฒฉ ๋ก๊ทธ์ธ(22) ์ด๋ฆผ',a.remote_login_22?'์':'์๋์'],['ํ๋ฉด ๊ณต์ (5900) ์ด๋ฆผ',a.screen_share_5900?'์':'์๋์'],['๋์๋ณด๋ ๋ฐ์ธ๋',a.dashboard_bind?a.dashboard_bind+(a.dashboard_bind==='127.0.0.1'?' (์ด ๊ธฐ๊ธฐ ์์์๋ง)':''):'์์']]);
|
|
695
|
+
if(a.top&&a.top.length)h+='<div class="act" style="font-size:12px;color:var(--muted)">'+a.top.map(x=>esc(x.ts+' ยท '+x.service+' ยท '+x.remote+' ยท '+x.country)).join('<br>')+'</div>';
|
|
696
|
+
if(a.redline)h+='<div class="act" style="color:var(--muted);font-size:12px">'+esc(a.redline)+'</div>';
|
|
697
|
+
return h;}
|
|
698
|
+
if(a.data)return '<div class="act">'+esc(a.data)+'</div>';
|
|
699
|
+
return '<div class="act">'+esc(a.scan_ts?'์ต๊ทผ ์ ๊ฒ '+a.scan_ts:'')+'</div>'+kv([['์ํ',a.high],['์ฃผ์',a.med],['์ ๋ณด',a.info]])+(a.top&&a.top.length?'<div class="act">'+a.top.map(esc).join(' ยท ')+'</div>':'');}
|
|
700
|
+
CARDS.cards.forEach((c,i)=>{
|
|
701
|
+
const el=document.createElement('div');el.className='mcard';
|
|
702
|
+
const subs=c.badge_sub||[];
|
|
703
|
+
if(subs.some(s=>s.indexOf('์ ๋ณด๊ฐ ์ค๋๋์ด์')===0||s==='์ต์ ์ ๋ณด ์ฐ๊ฒฐ์ด ๋๊ฒผ์ด์'))el.classList.add('stale');
|
|
704
|
+
const now=c.card==='threat_intel'&&c.actual.triage&&c.actual.triage['์ฆ์']>0;
|
|
705
|
+
if(now)el.classList.add('alert');
|
|
706
|
+
if(['์ง์ผ๋ณด๋ ์ค','์๋์ผ๋ก ๋ง๋ ์ค','๊ฒฝ๊ณ ๋ชจ๋','์ฐจ๋จ๋ชจ๋'].includes(c.badge))el.classList.add('live');
|
|
707
|
+
let more='';
|
|
708
|
+
if(c.card==='threat_intel'&&c.actual.items&&c.actual.items.length)more='<div class="moreb">'+c.actual.items.map(x=>'<div>['+esc(x.grade)+'] '+esc(x.pkg)+' '+esc(x.ver)+' ('+esc(x.eco)+')'+(x.kev?' ยท KEV':'')+' ยท CVE '+x.cves+'๊ฑด</div>').join('')+'</div>';
|
|
709
|
+
if(c.card==='agent_injection')more='<div class="moreb">'+(c.layers?'<div>๋ด๋ถ ์ํ(์ ๋ฌธ๊ฐ์ฉ): '+Object.keys(c.layers).map(k=>esc(k)+'='+esc(c.layers[k])).join(' ยท ')+'</div>':'')+esc(c.edu)+'</div>';
|
|
710
|
+
el.innerHTML='<div class="mh2"><span class="anum">'+(i+1)+'</span><b>'+esc(c.title)+'</b>'+
|
|
711
|
+
'<span class="cbadge '+bcls(c.badge)+'" title="'+esc(c.badge_desc||'')+'">'+esc(c.badge)+'</span>'+subs.map(s=>'<span class="cbadge '+bcls(s)+'">'+esc(s)+'</span>').join('')+'</div>'+
|
|
712
|
+
(c.badge==='์ค๋น ์ค'?'<div class="act" style="color:var(--muted)">'+esc(c.badge_desc)+'</div>':'')+
|
|
713
|
+
'<div class="fld"><i>โ ์งํค๋ ๊ฒ</i>'+esc(c.protects)+'</div>'+
|
|
714
|
+
'<div class="fld"><i>โก ๋์</i>'+esc(c.how)+'</div>'+
|
|
715
|
+
'<div class="fld"><i>โข ์ค์ ๋ก ํ ์ผ</i></div>'+actual(c)+
|
|
716
|
+
'<div class="fld lim"><i>โฃ ๋ชป ํ๋ ๊ฒ</i>'+esc(c.limits)+'</div>'+
|
|
717
|
+
(c.fixed?'<div class="fld"><i>โค ๊ทธ๋์ ์ด๋ ๊ฒ ๋ณด์ํ์ด์</i>'+esc(c.fixed)+'</div>':'')+
|
|
718
|
+
(c.ops?'<div class="fld"><i>โฅ ์ง๊ธ์ ์ด๋ ๊ฒ ์ด์๋ผ์</i>'+esc(c.ops)+'</div>':'')+
|
|
719
|
+
(more?'<button class="btn more" aria-expanded="false">'+(c.card==='agent_injection'?'๊ฐ์ ํ๋กฌํํธ ์ธ์ ์
์ด๋?':'์ฆ์ยท์ฐ์ ๋ชฉ๋ก '+c.actual.items.length+'๊ฑด')+'</button>'+more:'');
|
|
720
|
+
const b=el.querySelector('.more');
|
|
721
|
+
if(b)b.onclick=()=>{const o=el.classList.toggle('open');b.setAttribute('aria-expanded',o);};
|
|
722
|
+
G.appendChild(el);});
|
|
723
|
+
})();
|
|
724
|
+
|
|
725
|
+
// actions
|
|
726
|
+
document.getElementById('exportBtn').onclick=()=>{
|
|
727
|
+
const blob=new Blob([JSON.stringify(DATA,null,2)],{type:'application/json'});
|
|
728
|
+
const a=document.createElement('a');a.href=URL.createObjectURL(blob);
|
|
729
|
+
a.download='security_report_'+DATA.ts.replace(/[^0-9]/g,'')+'.json';a.click();};
|
|
730
|
+
document.getElementById('printBtn').onclick=()=>window.print();
|
|
731
|
+
const modal=document.getElementById('modal');
|
|
732
|
+
document.getElementById('localBadge').onclick=()=>{document.getElementById('mTitle').textContent='๐ '+UI.local;
|
|
733
|
+
document.getElementById('mBody').textContent=UI.local_more;modal.hidden=false;};
|
|
734
|
+
document.getElementById('mClose').onclick=()=>modal.hidden=true;
|
|
735
|
+
modal.onclick=e=>{if(e.target===modal)modal.hidden=true;};
|
|
736
|
+
document.addEventListener('keydown',e=>{if(e.key==='Escape')modal.hidden=true;});
|
|
737
|
+
document.title=UI.brand;
|
|
738
|
+
</script></body></html>"""
|
|
739
|
+
|
|
740
|
+
def render_html(rep):
|
|
741
|
+
uid = dict(UISTR[rep['lang']]); uid['areas'] = {k: AREA[k][rep['lang']] for k in AREA}
|
|
742
|
+
checks = {k: CHECKS[k][rep['lang']] for k in CHECKS}
|
|
743
|
+
data = json.dumps(rep, ensure_ascii=False).replace('<', '\\u003c')
|
|
744
|
+
ui = json.dumps(uid, ensure_ascii=False).replace('<', '\\u003c')
|
|
745
|
+
chk = json.dumps(checks, ensure_ascii=False).replace('<', '\\u003c')
|
|
746
|
+
cards = 'null'
|
|
747
|
+
if rep['lang'] == 'ko': # ๋ณด์ ๋ชจ๋ ์นด๋๋ ํ๊ตญ์ด ์ ์ฉยท์์ฑ๊ธฐ๊ฐ ์์ผ๋ฉด(๋ฐฐํฌ๋ณธ) ์น์
์๋ต
|
|
748
|
+
try:
|
|
749
|
+
import security_cards
|
|
750
|
+
cards = json.dumps(security_cards.build_cards(rep), ensure_ascii=False).replace('<', '\\u003c')
|
|
751
|
+
except Exception:
|
|
752
|
+
cards = 'null'
|
|
753
|
+
return (_DASHBOARD.replace('__L__', rep['lang']).replace('/*DATA*/', data)
|
|
754
|
+
.replace('/*UI*/', ui).replace('/*CHECKS*/', chk).replace('/*CARDS*/', cards))
|
|
755
|
+
|
|
756
|
+
def main():
|
|
757
|
+
ap = argparse.ArgumentParser()
|
|
758
|
+
ap.add_argument('--lang', default='ko', choices=['ko','en'])
|
|
759
|
+
ap.add_argument('--home', default=HOME)
|
|
760
|
+
outdir = os.getcwd() # ์ค์นํ CLI: ๊ฒฐ๊ณผ๋ฌผ์ ํ์ฌ ํด๋์
|
|
761
|
+
ap.add_argument('--json', default=os.path.join(outdir, 'scan_result.json'))
|
|
762
|
+
ap.add_argument('--html', default=os.path.join(outdir, 'security_report.html'))
|
|
763
|
+
ap.add_argument('--backup-repos', default='', help='git repo paths (colon-separated) counted as backup evidence')
|
|
764
|
+
a = ap.parse_args()
|
|
765
|
+
if a.backup_repos:
|
|
766
|
+
os.environ['SECSCAN_BACKUP_REPOS'] = a.backup_repos
|
|
767
|
+
rep = Scan(a.home, a.lang).run_all()
|
|
768
|
+
json.dump(rep, open(a.json,'w'), ensure_ascii=False, indent=1)
|
|
769
|
+
open(a.html,'w',encoding='utf-8').write(render_html(rep))
|
|
770
|
+
print(UI['done'][rep['lang']].format(s=rep['score'], h=rep['high'], m=rep['med'], n=len(rep['findings'])))
|
|
771
|
+
print(f"{UI['repline'][rep['lang']]}: {a.html}")
|
|
772
|
+
print(UI['zero'][rep['lang']])
|
|
773
|
+
|
|
774
|
+
if __name__ == '__main__':
|
|
775
|
+
main()
|