vibe-coding-qa 0.5.0__py3-none-any.whl
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.
- dashboard/__init__.py +10 -0
- dashboard/backend/__init__.py +11 -0
- dashboard/backend/__main__.py +6 -0
- dashboard/backend/ai/__init__.py +6 -0
- dashboard/backend/ai/provider.py +102 -0
- dashboard/backend/ai/service.py +757 -0
- dashboard/backend/api/__init__.py +5 -0
- dashboard/backend/api/ai.py +142 -0
- dashboard/backend/api/dashboard.py +34 -0
- dashboard/backend/api/deps.py +51 -0
- dashboard/backend/api/findings.py +102 -0
- dashboard/backend/api/intel.py +41 -0
- dashboard/backend/api/repositories.py +62 -0
- dashboard/backend/api/scans.py +168 -0
- dashboard/backend/api/system.py +69 -0
- dashboard/backend/config.py +180 -0
- dashboard/backend/main.py +183 -0
- dashboard/backend/persistence/__init__.py +5 -0
- dashboard/backend/persistence/database.py +278 -0
- dashboard/backend/persistence/store.py +600 -0
- dashboard/backend/profiles.py +141 -0
- dashboard/backend/schemas.py +452 -0
- dashboard/backend/security.py +158 -0
- dashboard/backend/services/__init__.py +7 -0
- dashboard/backend/services/dashboard.py +172 -0
- dashboard/backend/services/intel.py +341 -0
- dashboard/backend/services/paths.py +38 -0
- dashboard/backend/services/repositories.py +123 -0
- dashboard/backend/services/scans.py +328 -0
- dashboard/backend/services/settings.py +49 -0
- dashboard/frontend/dist/assets/index-COC-CdqD.js +492 -0
- dashboard/frontend/dist/assets/index-hWDLRaLr.css +1 -0
- dashboard/frontend/dist/index.html +29 -0
- dashboard/frontend/dist/phone-mascots.gif +0 -0
- dashboard/launcher.py +48 -0
- vibe_coding_qa/__init__.py +140 -0
- vibe_coding_qa/__main__.py +5 -0
- vibe_coding_qa/ai_review.py +165 -0
- vibe_coding_qa/cli.py +157 -0
- vibe_coding_qa/engine.py +860 -0
- vibe_coding_qa/fixes.py +222 -0
- vibe_coding_qa/gates.py +300 -0
- vibe_coding_qa/profile.py +471 -0
- vibe_coding_qa/repo.py +188 -0
- vibe_coding_qa/reporters/__init__.py +253 -0
- vibe_coding_qa/scanners/__init__.py +195 -0
- vibe_coding_qa/scanners/compliance.py +167 -0
- vibe_coding_qa/scanners/database.py +316 -0
- vibe_coding_qa/scanners/dependencies.py +595 -0
- vibe_coding_qa/scanners/infrastructure.py +125 -0
- vibe_coding_qa/scanners/performance.py +84 -0
- vibe_coding_qa/scanners/quality.py +129 -0
- vibe_coding_qa/scanners/security.py +521 -0
- vibe_coding_qa-0.5.0.dist-info/METADATA +323 -0
- vibe_coding_qa-0.5.0.dist-info/RECORD +58 -0
- vibe_coding_qa-0.5.0.dist-info/WHEEL +5 -0
- vibe_coding_qa-0.5.0.dist-info/entry_points.txt +3 -0
- vibe_coding_qa-0.5.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Infrastructure scanner: TLS, headers, container, nginx configs."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from . import BaseScanner, load_ignore_patterns, is_ignored
|
|
7
|
+
from vibe_coding_qa import Finding, Severity
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InfrastructureScanner(BaseScanner):
|
|
11
|
+
module = "infrastructure"
|
|
12
|
+
|
|
13
|
+
def scan(self) -> list[Finding]:
|
|
14
|
+
ignore = load_ignore_patterns(self.repo)
|
|
15
|
+
self._scan_nginx(ignore)
|
|
16
|
+
self._scan_docker(ignore)
|
|
17
|
+
self._scan_csp(ignore)
|
|
18
|
+
return self.findings
|
|
19
|
+
|
|
20
|
+
def _scan_nginx(self, ignore: list[str]) -> None:
|
|
21
|
+
for path in self.repo.rglob("*.conf"):
|
|
22
|
+
if is_ignored(path, self.repo, ignore):
|
|
23
|
+
continue
|
|
24
|
+
if "nginx" not in path.name.lower() and "default" not in path.name.lower():
|
|
25
|
+
continue
|
|
26
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
27
|
+
lowered = text.lower()
|
|
28
|
+
for i, raw in enumerate(text.splitlines(), 1):
|
|
29
|
+
norm = raw.strip().lower()
|
|
30
|
+
if "listen 80" in norm and "redirect" not in lowered:
|
|
31
|
+
self.add(
|
|
32
|
+
rule="nginx-plain-http-listen",
|
|
33
|
+
severity=Severity.HIGH,
|
|
34
|
+
file=path,
|
|
35
|
+
line=i,
|
|
36
|
+
message="Nginx listens on plain HTTP without an obvious redirect to HTTPS.",
|
|
37
|
+
suggestion="Redirect all HTTP traffic to HTTPS before serving content.",
|
|
38
|
+
confidence="medium",
|
|
39
|
+
)
|
|
40
|
+
if re.search(r"ssl_protocols\s+.*(sslv2|sslv3|tlsv1\.0|tlsv1\.1)", raw, re.IGNORECASE):
|
|
41
|
+
self.add(
|
|
42
|
+
rule="nginx-weak-tls",
|
|
43
|
+
severity=Severity.CRITICAL,
|
|
44
|
+
file=path,
|
|
45
|
+
line=i,
|
|
46
|
+
message="Nginx enables a deprecated TLS/SSL protocol.",
|
|
47
|
+
suggestion="Use only TLS 1.2 and TLS 1.3.",
|
|
48
|
+
confidence="high",
|
|
49
|
+
)
|
|
50
|
+
if "strict-transport-security" not in lowered:
|
|
51
|
+
self.add(
|
|
52
|
+
rule="nginx-missing-hsts",
|
|
53
|
+
severity=Severity.HIGH,
|
|
54
|
+
file=path,
|
|
55
|
+
line=1,
|
|
56
|
+
message="HSTS header not detected in Nginx config.",
|
|
57
|
+
suggestion="Add add_header Strict-Transport-Security with max-age=63072000; includeSubDomains; preload.",
|
|
58
|
+
confidence="medium",
|
|
59
|
+
)
|
|
60
|
+
for header in ("x-frame-options", "x-content-type-options", "referrer-policy", "permissions-policy"):
|
|
61
|
+
if header not in lowered:
|
|
62
|
+
self.add(
|
|
63
|
+
rule=f"nginx-missing-{header.replace('-', '_')}",
|
|
64
|
+
severity=Severity.MEDIUM,
|
|
65
|
+
file=path,
|
|
66
|
+
line=1,
|
|
67
|
+
message=f"{header.upper().replace('-', '-')} header not detected in Nginx config.",
|
|
68
|
+
suggestion=f"Add the {header.upper().replace('-', '-')} header to every response.",
|
|
69
|
+
confidence="low",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def _scan_docker(self, ignore: list[str]) -> None:
|
|
73
|
+
for path in self.repo.rglob("Dockerfile*"):
|
|
74
|
+
if is_ignored(path, self.repo, ignore):
|
|
75
|
+
continue
|
|
76
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
77
|
+
for i, raw in enumerate(text.splitlines(), 1):
|
|
78
|
+
if re.search(r"^FROM\s+.*:latest", raw, re.IGNORECASE):
|
|
79
|
+
self.add(
|
|
80
|
+
rule="docker-latest-tag",
|
|
81
|
+
severity=Severity.LOW,
|
|
82
|
+
file=path,
|
|
83
|
+
line=i,
|
|
84
|
+
message="Docker base image uses the :latest tag.",
|
|
85
|
+
suggestion="Pin a specific digest or version tag for reproducible builds.",
|
|
86
|
+
confidence="medium",
|
|
87
|
+
)
|
|
88
|
+
if re.search(r"EXPOSE\s+\d+", raw, re.IGNORECASE) and "EXPOSE 443" not in raw.upper():
|
|
89
|
+
self.add(
|
|
90
|
+
rule="docker-exposed-port-review",
|
|
91
|
+
severity=Severity.INFO,
|
|
92
|
+
file=path,
|
|
93
|
+
line=i,
|
|
94
|
+
message="Dockerfile exposes a port; confirm TLS terminates at the edge or reverse proxy.",
|
|
95
|
+
suggestion="Terminate TLS at the reverse proxy or load balancer; do not expose plain HTTP directly.",
|
|
96
|
+
confidence="low",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _scan_csp(self, ignore: list[str]) -> None:
|
|
100
|
+
for path in self.iter_source_files((".ts", ".tsx", ".js", ".jsx", ".py")):
|
|
101
|
+
if is_ignored(path, self.repo, ignore):
|
|
102
|
+
continue
|
|
103
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
104
|
+
for i, raw in enumerate(text.splitlines(), 1):
|
|
105
|
+
if "content-security-policy" in raw.lower():
|
|
106
|
+
if "unsafe-inline" in raw.lower() or "unsafe-eval" in raw.lower():
|
|
107
|
+
self.add(
|
|
108
|
+
rule="csp-unsafe-directive",
|
|
109
|
+
severity=Severity.MEDIUM,
|
|
110
|
+
file=path,
|
|
111
|
+
line=i,
|
|
112
|
+
message="CSP uses unsafe-inline or unsafe-eval.",
|
|
113
|
+
suggestion="Use nonces or hashes for scripts and remove unsafe-eval where possible.",
|
|
114
|
+
confidence="medium",
|
|
115
|
+
)
|
|
116
|
+
return
|
|
117
|
+
self.add(
|
|
118
|
+
rule="csp-missing",
|
|
119
|
+
severity=Severity.MEDIUM,
|
|
120
|
+
file=self.repo,
|
|
121
|
+
line=1,
|
|
122
|
+
message="No Content-Security-Policy header detected in source.",
|
|
123
|
+
suggestion="Add a restrictive CSP through Nginx or application middleware.",
|
|
124
|
+
confidence="low",
|
|
125
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Performance scanner: bundle/debug artifacts, large assets, INP hints."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from . import BaseScanner, load_ignore_patterns, is_ignored, is_likely_client_file
|
|
7
|
+
from vibe_coding_qa import Finding, Severity
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PerformanceScanner(BaseScanner):
|
|
11
|
+
module = "performance"
|
|
12
|
+
LARGE_FILE_BYTES = 500_000
|
|
13
|
+
LARGE_IMAGE_BYTES = 500_000
|
|
14
|
+
|
|
15
|
+
def scan(self) -> list[Finding]:
|
|
16
|
+
ignore = load_ignore_patterns(self.repo)
|
|
17
|
+
for path in self.iter_source_files((".ts", ".tsx", ".js", ".jsx", ".py")):
|
|
18
|
+
if is_ignored(path, self.repo, ignore):
|
|
19
|
+
continue
|
|
20
|
+
self._scan_source(path)
|
|
21
|
+
for path in self.repo.rglob("*"):
|
|
22
|
+
if not path.is_file():
|
|
23
|
+
continue
|
|
24
|
+
if "node_modules" in path.parts or ".git" in path.parts:
|
|
25
|
+
continue
|
|
26
|
+
if is_ignored(path, self.repo, ignore):
|
|
27
|
+
continue
|
|
28
|
+
self._scan_asset(path)
|
|
29
|
+
return self.findings
|
|
30
|
+
|
|
31
|
+
def _scan_source(self, path: Path) -> None:
|
|
32
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
33
|
+
for i, raw in enumerate(text.splitlines(), 1):
|
|
34
|
+
if re.search(r"scheduler\.yield\s*\(\)|yield\s+to\s+main", raw, re.IGNORECASE):
|
|
35
|
+
# Positive signal; skip further INP warnings in this file.
|
|
36
|
+
return
|
|
37
|
+
if re.search(r"(onClick|onSubmit|onChange)\s*=\s*\{?\s*(async\s+)?function", raw, re.IGNORECASE):
|
|
38
|
+
# Heuristic: handlers that do heavy work without yielding.
|
|
39
|
+
pass
|
|
40
|
+
# Large synchronous-looking loops in handlers.
|
|
41
|
+
for i, raw in enumerate(text.splitlines(), 1):
|
|
42
|
+
if re.search(r"for\s*\([^)]*\)\s*\{[^}]*\b(fetch|await|compute|process)\b", raw, re.IGNORECASE):
|
|
43
|
+
if is_likely_client_file(path):
|
|
44
|
+
self.add(
|
|
45
|
+
rule="potential-blocking-handler",
|
|
46
|
+
severity=Severity.LOW,
|
|
47
|
+
file=path,
|
|
48
|
+
line=i,
|
|
49
|
+
message="Loop in an event handler may block the main thread.",
|
|
50
|
+
suggestion="Yield to the main thread with scheduler.yield() or break work into chunks.",
|
|
51
|
+
confidence="low",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def _scan_asset(self, path: Path) -> None:
|
|
55
|
+
if path.suffix.lower() in {".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"}:
|
|
56
|
+
try:
|
|
57
|
+
size = path.stat().st_size
|
|
58
|
+
except OSError:
|
|
59
|
+
return
|
|
60
|
+
if size > self.LARGE_IMAGE_BYTES:
|
|
61
|
+
self.add(
|
|
62
|
+
rule="large-unoptimized-image",
|
|
63
|
+
severity=Severity.LOW,
|
|
64
|
+
file=path,
|
|
65
|
+
line=1,
|
|
66
|
+
message=f"Image is {size / 1024:.0f} KB; consider compression or modern formats.",
|
|
67
|
+
suggestion="Compress images, serve WebP/AVIF, and use responsive sizes.",
|
|
68
|
+
confidence="high",
|
|
69
|
+
)
|
|
70
|
+
elif path.suffix.lower() in {".js", ".css", ".ts", ".tsx"}:
|
|
71
|
+
try:
|
|
72
|
+
size = path.stat().st_size
|
|
73
|
+
except OSError:
|
|
74
|
+
return
|
|
75
|
+
if size > self.LARGE_FILE_BYTES:
|
|
76
|
+
self.add(
|
|
77
|
+
rule="large-source-file",
|
|
78
|
+
severity=Severity.LOW,
|
|
79
|
+
file=path,
|
|
80
|
+
line=1,
|
|
81
|
+
message=f"Source file is {size / 1024:.0f} KB; consider splitting.",
|
|
82
|
+
suggestion="Split large modules to improve loading and caching.",
|
|
83
|
+
confidence="low",
|
|
84
|
+
)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Quality scanner: TypeScript/JavaScript/Python static checks."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from . import BaseScanner, load_ignore_patterns, is_ignored, is_likely_client_file, is_test_or_fixture
|
|
7
|
+
from vibe_coding_qa import Finding, Severity
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class QualityScanner(BaseScanner):
|
|
11
|
+
module = "quality"
|
|
12
|
+
|
|
13
|
+
def scan(self) -> list[Finding]:
|
|
14
|
+
ignore = load_ignore_patterns(self.repo)
|
|
15
|
+
for path in self.iter_source_files((".ts", ".tsx", ".js", ".jsx", ".py")):
|
|
16
|
+
if is_ignored(path, self.repo, ignore):
|
|
17
|
+
continue
|
|
18
|
+
self._scan_console_logs(path)
|
|
19
|
+
self._scan_todos(path)
|
|
20
|
+
self._scan_any_types(path)
|
|
21
|
+
self._scan_missing_error_boundaries(path)
|
|
22
|
+
self._scan_debugger_statements(path)
|
|
23
|
+
self._scan_build_config(ignore)
|
|
24
|
+
return self.findings
|
|
25
|
+
|
|
26
|
+
def _scan_console_logs(self, path: Path) -> None:
|
|
27
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
28
|
+
for i, raw in enumerate(text.splitlines(), 1):
|
|
29
|
+
if re.search(r"(?<!\w)console\.(log|debug|warn)\s*\(", raw):
|
|
30
|
+
if is_test_or_fixture(path):
|
|
31
|
+
continue
|
|
32
|
+
self.add(
|
|
33
|
+
rule="console-log-in-production",
|
|
34
|
+
severity=Severity.LOW,
|
|
35
|
+
file=path,
|
|
36
|
+
line=i,
|
|
37
|
+
message="Console logging statement found in source code.",
|
|
38
|
+
suggestion="Remove console.log/debug/warn from production builds or route through a logging service.",
|
|
39
|
+
confidence="high",
|
|
40
|
+
fixable=True,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def _scan_todos(self, path: Path) -> None:
|
|
44
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
45
|
+
for i, raw in enumerate(text.splitlines(), 1):
|
|
46
|
+
if re.search(r"(?<!\w)(TODO|FIXME|HACK|XXX)\s*[:\(]", raw, re.IGNORECASE):
|
|
47
|
+
self.add(
|
|
48
|
+
rule="unfinished-todo",
|
|
49
|
+
severity=Severity.LOW,
|
|
50
|
+
file=path,
|
|
51
|
+
line=i,
|
|
52
|
+
message="Unresolved TODO/FIXME/HACK marker found.",
|
|
53
|
+
suggestion="Resolve or ticket the item before release.",
|
|
54
|
+
confidence="high",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def _scan_any_types(self, path: Path) -> None:
|
|
58
|
+
if path.suffix not in {".ts", ".tsx"}:
|
|
59
|
+
return
|
|
60
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
61
|
+
for i, raw in enumerate(text.splitlines(), 1):
|
|
62
|
+
if ": any" in raw or "as any" in raw or "<any>" in raw:
|
|
63
|
+
self.add(
|
|
64
|
+
rule="unsafe-any-type",
|
|
65
|
+
severity=Severity.MEDIUM,
|
|
66
|
+
file=path,
|
|
67
|
+
line=i,
|
|
68
|
+
message="Unsafe `any` type assertion detected.",
|
|
69
|
+
suggestion="Replace `any` with a specific interface or use `unknown` with a type guard.",
|
|
70
|
+
confidence="medium",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def _scan_missing_error_boundaries(self, path: Path) -> None:
|
|
74
|
+
if not is_likely_client_file(path) or path.suffix not in {".tsx", ".jsx"}:
|
|
75
|
+
return
|
|
76
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
77
|
+
# Only flag if a component file has no error boundary and is reasonably large.
|
|
78
|
+
if "ErrorBoundary" in text or "error boundary" in text.lower():
|
|
79
|
+
return
|
|
80
|
+
if len(text.splitlines()) < 40:
|
|
81
|
+
return
|
|
82
|
+
self.add(
|
|
83
|
+
rule="missing-error-boundary",
|
|
84
|
+
severity=Severity.MEDIUM,
|
|
85
|
+
file=path,
|
|
86
|
+
line=1,
|
|
87
|
+
message="No React ErrorBoundary detected in this component file.",
|
|
88
|
+
suggestion="Add an error boundary to prevent a single component crash from taking down the whole UI.",
|
|
89
|
+
confidence="low",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def _scan_debugger_statements(self, path: Path) -> None:
|
|
93
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
94
|
+
for i, raw in enumerate(text.splitlines(), 1):
|
|
95
|
+
if re.search(r"(?<!\w)debugger;?\s*$", raw):
|
|
96
|
+
self.add(
|
|
97
|
+
rule="debugger-statement",
|
|
98
|
+
severity=Severity.LOW,
|
|
99
|
+
file=path,
|
|
100
|
+
line=i,
|
|
101
|
+
message="`debugger` statement left in source.",
|
|
102
|
+
suggestion="Remove debugger statements before production builds.",
|
|
103
|
+
confidence="high",
|
|
104
|
+
fixable=True,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def _scan_build_config(self, ignore: list[str]) -> None:
|
|
108
|
+
vite = self.repo / "vite.config.ts"
|
|
109
|
+
next_config = self.repo / "next.config.js"
|
|
110
|
+
for path in (vite, next_config):
|
|
111
|
+
if not path.is_file():
|
|
112
|
+
continue
|
|
113
|
+
if is_ignored(path, self.repo, ignore):
|
|
114
|
+
continue
|
|
115
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
116
|
+
for i, raw in enumerate(text.splitlines(), 1):
|
|
117
|
+
if "sourcemap" in raw.lower() and "false" not in raw.lower():
|
|
118
|
+
self.add(
|
|
119
|
+
rule="source-maps-enabled",
|
|
120
|
+
severity=Severity.LOW,
|
|
121
|
+
file=path,
|
|
122
|
+
line=i,
|
|
123
|
+
message="Source maps may be enabled in production build config.",
|
|
124
|
+
suggestion="Disable source maps in production to avoid exposing source code.",
|
|
125
|
+
confidence="low",
|
|
126
|
+
)
|
|
127
|
+
if "console" in raw.lower() and "drop" in raw.lower():
|
|
128
|
+
# Config removes console logs; clear prior findings is done in engine, not here.
|
|
129
|
+
pass
|