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.
Files changed (58) hide show
  1. dashboard/__init__.py +10 -0
  2. dashboard/backend/__init__.py +11 -0
  3. dashboard/backend/__main__.py +6 -0
  4. dashboard/backend/ai/__init__.py +6 -0
  5. dashboard/backend/ai/provider.py +102 -0
  6. dashboard/backend/ai/service.py +757 -0
  7. dashboard/backend/api/__init__.py +5 -0
  8. dashboard/backend/api/ai.py +142 -0
  9. dashboard/backend/api/dashboard.py +34 -0
  10. dashboard/backend/api/deps.py +51 -0
  11. dashboard/backend/api/findings.py +102 -0
  12. dashboard/backend/api/intel.py +41 -0
  13. dashboard/backend/api/repositories.py +62 -0
  14. dashboard/backend/api/scans.py +168 -0
  15. dashboard/backend/api/system.py +69 -0
  16. dashboard/backend/config.py +180 -0
  17. dashboard/backend/main.py +183 -0
  18. dashboard/backend/persistence/__init__.py +5 -0
  19. dashboard/backend/persistence/database.py +278 -0
  20. dashboard/backend/persistence/store.py +600 -0
  21. dashboard/backend/profiles.py +141 -0
  22. dashboard/backend/schemas.py +452 -0
  23. dashboard/backend/security.py +158 -0
  24. dashboard/backend/services/__init__.py +7 -0
  25. dashboard/backend/services/dashboard.py +172 -0
  26. dashboard/backend/services/intel.py +341 -0
  27. dashboard/backend/services/paths.py +38 -0
  28. dashboard/backend/services/repositories.py +123 -0
  29. dashboard/backend/services/scans.py +328 -0
  30. dashboard/backend/services/settings.py +49 -0
  31. dashboard/frontend/dist/assets/index-COC-CdqD.js +492 -0
  32. dashboard/frontend/dist/assets/index-hWDLRaLr.css +1 -0
  33. dashboard/frontend/dist/index.html +29 -0
  34. dashboard/frontend/dist/phone-mascots.gif +0 -0
  35. dashboard/launcher.py +48 -0
  36. vibe_coding_qa/__init__.py +140 -0
  37. vibe_coding_qa/__main__.py +5 -0
  38. vibe_coding_qa/ai_review.py +165 -0
  39. vibe_coding_qa/cli.py +157 -0
  40. vibe_coding_qa/engine.py +860 -0
  41. vibe_coding_qa/fixes.py +222 -0
  42. vibe_coding_qa/gates.py +300 -0
  43. vibe_coding_qa/profile.py +471 -0
  44. vibe_coding_qa/repo.py +188 -0
  45. vibe_coding_qa/reporters/__init__.py +253 -0
  46. vibe_coding_qa/scanners/__init__.py +195 -0
  47. vibe_coding_qa/scanners/compliance.py +167 -0
  48. vibe_coding_qa/scanners/database.py +316 -0
  49. vibe_coding_qa/scanners/dependencies.py +595 -0
  50. vibe_coding_qa/scanners/infrastructure.py +125 -0
  51. vibe_coding_qa/scanners/performance.py +84 -0
  52. vibe_coding_qa/scanners/quality.py +129 -0
  53. vibe_coding_qa/scanners/security.py +521 -0
  54. vibe_coding_qa-0.5.0.dist-info/METADATA +323 -0
  55. vibe_coding_qa-0.5.0.dist-info/RECORD +58 -0
  56. vibe_coding_qa-0.5.0.dist-info/WHEEL +5 -0
  57. vibe_coding_qa-0.5.0.dist-info/entry_points.txt +3 -0
  58. vibe_coding_qa-0.5.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,141 @@
1
+ """Scan profiles.
2
+
3
+ A profile is not a label: it selects which scanner modules actually run, whether
4
+ the isolated test suite and OSV lookups are attempted, whether optional AI review
5
+ is requested, and the minimum score the release gate enforces. All of these map
6
+ onto real engine parameters.
7
+ """
8
+
9
+ from dataclasses import dataclass
10
+
11
+ ALL_MODULES: tuple[str, ...] = (
12
+ "security",
13
+ "quality",
14
+ "database",
15
+ "infrastructure",
16
+ "performance",
17
+ "compliance",
18
+ "dependencies",
19
+ )
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class ScanProfile:
24
+ key: str
25
+ name: str
26
+ description: str
27
+ modules: tuple[str, ...]
28
+ run_tests: bool
29
+ osv: bool
30
+ ai_review: bool
31
+ minimum_score: int
32
+ notes: tuple[str, ...] = ()
33
+
34
+ def to_dict(self) -> dict:
35
+ return {
36
+ "key": self.key,
37
+ "name": self.name,
38
+ "description": self.description,
39
+ "modules": list(self.modules),
40
+ "run_tests": self.run_tests,
41
+ "osv": self.osv,
42
+ "ai_review": self.ai_review,
43
+ "minimum_score": self.minimum_score,
44
+ "notes": list(self.notes),
45
+ }
46
+
47
+
48
+ PROFILES: dict[str, ScanProfile] = {
49
+ "quick": ScanProfile(
50
+ key="quick",
51
+ name="Quick Scan",
52
+ description="Fast static pass over security and dependency risks. No tests, no network lookups.",
53
+ modules=("security", "dependencies"),
54
+ run_tests=False,
55
+ osv=False,
56
+ ai_review=False,
57
+ minimum_score=50,
58
+ notes=("Static analysis only.", "No test execution.", "No OSV advisory lookups."),
59
+ ),
60
+ "standard": ScanProfile(
61
+ key="standard",
62
+ name="Standard Scan",
63
+ description="All static scanners plus optional OSV advisory lookups and isolated tests when permitted.",
64
+ modules=ALL_MODULES,
65
+ run_tests=True,
66
+ osv=True,
67
+ ai_review=False,
68
+ minimum_score=60,
69
+ notes=("Runs every static scanner.", "Tests run only with explicit permission."),
70
+ ),
71
+ "deep": ScanProfile(
72
+ key="deep",
73
+ name="Deep Scan",
74
+ description="Everything in Standard plus optional AI review of critical and high findings.",
75
+ modules=ALL_MODULES,
76
+ run_tests=True,
77
+ osv=True,
78
+ ai_review=True,
79
+ minimum_score=60,
80
+ notes=("Requires a configured AI provider for the AI stage.",),
81
+ ),
82
+ "production_readiness": ScanProfile(
83
+ key="production_readiness",
84
+ name="Production Readiness",
85
+ description="Full audit with a stricter release threshold; unverified tests block a PASS.",
86
+ modules=ALL_MODULES,
87
+ run_tests=True,
88
+ osv=True,
89
+ ai_review=False,
90
+ minimum_score=75,
91
+ notes=("Minimum score 75.", "Requires a verified test run for a PASS verdict."),
92
+ ),
93
+ "security_audit": ScanProfile(
94
+ key="security_audit",
95
+ name="Security Audit",
96
+ description="Focused on exploitable risk: security, database exposure, infrastructure and compliance.",
97
+ modules=("security", "database", "infrastructure", "compliance", "dependencies"),
98
+ run_tests=False,
99
+ osv=True,
100
+ ai_review=True,
101
+ minimum_score=65,
102
+ notes=("Skips quality and performance scanners.",),
103
+ ),
104
+ "supabase_audit": ScanProfile(
105
+ key="supabase_audit",
106
+ name="Supabase Audit",
107
+ description="Row-level-security and data-exposure focused audit.",
108
+ modules=("database", "security", "compliance", "infrastructure"),
109
+ run_tests=False,
110
+ osv=False,
111
+ ai_review=False,
112
+ minimum_score=60,
113
+ notes=("Emphasises Postgres RLS and client-side data exposure.",),
114
+ ),
115
+ "nextjs_audit": ScanProfile(
116
+ key="nextjs_audit",
117
+ name="Next.js Audit",
118
+ description="Frontend/API security, quality and performance for Next.js and React applications.",
119
+ modules=("security", "quality", "dependencies", "performance", "infrastructure", "compliance"),
120
+ run_tests=True,
121
+ osv=True,
122
+ ai_review=False,
123
+ minimum_score=60,
124
+ notes=("Runs the npm/vitest/jest suite only with explicit permission.",),
125
+ ),
126
+ }
127
+
128
+ DEFAULT_PROFILE = "standard"
129
+
130
+
131
+ def get_profile(key: str | None) -> ScanProfile:
132
+ if not key:
133
+ return PROFILES[DEFAULT_PROFILE]
134
+ profile = PROFILES.get(key)
135
+ if profile is None:
136
+ raise KeyError(key)
137
+ return profile
138
+
139
+
140
+ def list_profiles() -> list[dict]:
141
+ return [profile.to_dict() for profile in PROFILES.values()]
@@ -0,0 +1,452 @@
1
+ """Request/response schemas for the dashboard API (Pydantic v2)."""
2
+
3
+ from datetime import datetime
4
+ from typing import Any, Literal
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
7
+
8
+
9
+ SourceType = Literal["local", "github"]
10
+ ScanStatus = Literal["queued", "running", "completed", "failed", "cancelled"]
11
+ FindingStatus = Literal["open", "reviewed", "resolved", "dismissed", "unverified"]
12
+ Severity = Literal["critical", "high", "medium", "low", "info"]
13
+
14
+
15
+ class ErrorResponse(BaseModel):
16
+ detail: str
17
+ code: str = "error"
18
+
19
+
20
+ # ------------------------------------------------------------------ repositories
21
+ class RepositoryCreate(BaseModel):
22
+ name: str | None = Field(default=None, max_length=200)
23
+ source: str = Field(min_length=1, max_length=500)
24
+ source_type: SourceType = "local"
25
+
26
+ @field_validator("source")
27
+ @classmethod
28
+ def _strip(cls, value: str) -> str:
29
+ return value.strip()
30
+
31
+
32
+ class LatestScanSummary(BaseModel):
33
+ id: int
34
+ status: ScanStatus
35
+ overall_score: float | None = None
36
+ gate: str | None = None
37
+ critical_count: int | None = None
38
+ high_count: int | None = None
39
+ completed_at: str | None = None
40
+ created_at: str | None = None
41
+ test_status: str | None = None
42
+
43
+
44
+ class RepositoryOut(BaseModel):
45
+ model_config = ConfigDict(from_attributes=True)
46
+
47
+ id: int
48
+ name: str
49
+ source: str
50
+ source_type: str
51
+ created_at: str
52
+ updated_at: str
53
+ main_language: str | None = None
54
+ frameworks: list[str] = Field(default_factory=list)
55
+ package_manager: str | None = None
56
+ database_tech: list[str] = Field(default_factory=list)
57
+ auth_tech: list[str] = Field(default_factory=list)
58
+ deployment: list[str] = Field(default_factory=list)
59
+ default_branch: str | None = None
60
+ last_commit_sha: str | None = None
61
+ last_scan_id: int | None = None
62
+ latest_scan: LatestScanSummary | None = None
63
+
64
+
65
+ # ------------------------------------------------------------------------- scans
66
+ class ScanOptions(BaseModel):
67
+ """Per-scan overrides.
68
+
69
+ Resource fields default to ``None`` so "not specified" is distinguishable
70
+ from "explicitly set": omitted values fall back to the operator's persisted
71
+ scan settings (see ``services.settings``), which is what makes the scan
72
+ settings screen functional rather than decorative.
73
+ """
74
+
75
+ static_only: bool = False
76
+ explicit_permission: bool = False
77
+ test_timeout: int | None = Field(default=None, ge=1, le=7200)
78
+ test_image: str | None = None
79
+ test_command: str | None = None
80
+ max_memory_mb: int | None = Field(default=None, ge=64, le=16384)
81
+ max_cpu_seconds: int | None = Field(default=None, ge=1, le=3600)
82
+ max_snapshot_mb: int | None = Field(default=None, ge=1, le=2048)
83
+ max_write_mb: int | None = Field(default=None, ge=2, le=2048)
84
+ osv: bool | None = None
85
+ ai_review: bool | None = None
86
+ share_source_context: bool = False
87
+
88
+
89
+ class ScanCreate(BaseModel):
90
+ repository_id: int
91
+ profile: str = "standard"
92
+ options: ScanOptions = Field(default_factory=ScanOptions)
93
+
94
+
95
+ class ScanOut(BaseModel):
96
+ model_config = ConfigDict(from_attributes=True)
97
+
98
+ id: int
99
+ repository_id: int
100
+ repository_name: str | None = None
101
+ repository_source: str | None = None
102
+ status: ScanStatus
103
+ profile: str
104
+ created_at: str
105
+ started_at: str | None = None
106
+ completed_at: str | None = None
107
+ commit_sha: str | None = None
108
+ overall_score: float | None = None
109
+ gate: str | None = None
110
+ critical_count: int | None = 0
111
+ high_count: int | None = 0
112
+ total_findings: int | None = 0
113
+ test_status: str | None = None
114
+ tests_executed: int | None = None
115
+ tests_passed: int | None = None
116
+ tests_failed: int | None = None
117
+ tests_skipped: int | None = None
118
+ duration_ms: int | None = None
119
+ scanner_versions: dict[str, Any] = Field(default_factory=dict)
120
+ coverage_gaps: list[str] = Field(default_factory=list)
121
+ ai_review_status: str | None = None
122
+ error: str | None = None
123
+ options: dict[str, Any] = Field(default_factory=dict)
124
+
125
+
126
+ class ScanListOut(BaseModel):
127
+ items: list[ScanOut]
128
+ total: int
129
+ limit: int
130
+ offset: int
131
+
132
+
133
+ class ScanEventOut(BaseModel):
134
+ id: int
135
+ scan_id: int
136
+ ts: str
137
+ stage: str
138
+ status: str | None = None
139
+ message: str | None = None
140
+ data: dict[str, Any] = Field(default_factory=dict)
141
+
142
+
143
+ # ---------------------------------------------------------------------- findings
144
+ class FindingOut(BaseModel):
145
+ model_config = ConfigDict(from_attributes=True)
146
+
147
+ id: int
148
+ scan_id: int
149
+ repository_id: int
150
+ repository_name: str | None = None
151
+ fingerprint: str
152
+ rule: str
153
+ module: str
154
+ severity: Severity
155
+ file: str
156
+ line: int
157
+ column: int = 0
158
+ message: str | None = ""
159
+ suggestion: str | None = ""
160
+ confidence: str | None = "medium"
161
+ fixable: int = 0
162
+ status: FindingStatus
163
+ first_seen_scan_id: int | None = None
164
+ created_at: str
165
+
166
+
167
+ class FindingDetailOut(FindingOut):
168
+ scan_profile: str | None = None
169
+ scan_completed_at: str | None = None
170
+ evidence: list[str] = Field(default_factory=list)
171
+ history: list[dict[str, Any]] = Field(default_factory=list)
172
+ related: list[dict[str, Any]] = Field(default_factory=list)
173
+ latest_ai_review: dict[str, Any] | None = None
174
+
175
+
176
+ class FindingListOut(BaseModel):
177
+ items: list[FindingOut]
178
+ total: int
179
+ limit: int
180
+ offset: int
181
+
182
+
183
+ class FindingStatusUpdate(BaseModel):
184
+ status: Literal["open", "reviewed", "dismissed", "unverified"]
185
+
186
+
187
+ # ---------------------------------------------------------------------- dashboard
188
+ class SummaryCards(BaseModel):
189
+ overall_score: float | None
190
+ critical: int
191
+ high: int
192
+ total_open: int
193
+ tests_executed: int | None
194
+ tests_passed: int | None
195
+ gate: str | None
196
+ repository_count: int
197
+ latest_scan_duration_ms: int | None
198
+
199
+
200
+ class OverviewOut(BaseModel):
201
+ summary: SummaryCards
202
+ severity_breakdown: dict[str, int]
203
+ category_breakdown: dict[str, int]
204
+ score_history: list[dict[str, Any]]
205
+ findings_trend: list[dict[str, Any]]
206
+ test_history: list[dict[str, Any]]
207
+ repository_health: list[dict[str, Any]]
208
+ has_data: bool
209
+
210
+
211
+ class ActivityItem(BaseModel):
212
+ kind: str
213
+ title: str
214
+ detail: str
215
+ at: str | None
216
+ repository_id: int | None = None
217
+ scan_id: int | None = None
218
+ finding_id: int | None = None
219
+ severity: str | None = None
220
+
221
+
222
+ class ActivityOut(BaseModel):
223
+ items: list[ActivityItem]
224
+
225
+
226
+ # ---------------------------------------------------------------------------- AI
227
+ class AIChatRequest(BaseModel):
228
+ message: str = Field(min_length=1, max_length=8000)
229
+ repository_id: int | None = None
230
+ conversation_id: int | None = None
231
+
232
+
233
+ class AIMessageOut(BaseModel):
234
+ id: int
235
+ role: str
236
+ content: str
237
+ created_at: str
238
+ metadata: dict[str, Any] = Field(default_factory=dict)
239
+
240
+
241
+ class AIChatResponse(BaseModel):
242
+ conversation_id: int
243
+ message: AIMessageOut
244
+ configured: bool
245
+ sources: list[dict[str, Any]] = Field(default_factory=list)
246
+
247
+
248
+ class AIConversationOut(BaseModel):
249
+ id: int
250
+ repository_id: int | None
251
+ title: str
252
+ created_at: str
253
+ updated_at: str
254
+ messages: list[AIMessageOut] = Field(default_factory=list)
255
+
256
+
257
+ class AIReviewRequest(BaseModel):
258
+ kind: Literal["analyst", "explain", "impact"] = "analyst"
259
+
260
+
261
+ class AIReviewOut(BaseModel):
262
+ finding_id: int
263
+ configured: bool
264
+ provider: str | None = None
265
+ model: str | None = None
266
+ error: str | None = None
267
+ content: dict[str, Any] = Field(default_factory=dict)
268
+
269
+
270
+ class RemediationProposeRequest(BaseModel):
271
+ finding_id: int
272
+
273
+
274
+ class RemediationOut(BaseModel):
275
+ id: int
276
+ finding_id: int | None
277
+ repository_id: int
278
+ status: str
279
+ created_at: str
280
+ updated_at: str
281
+ provider: str | None = None
282
+ model: str | None = None
283
+ file: str | None = None
284
+ original: str | None = None
285
+ proposed: str | None = None
286
+ diff: str | None = None
287
+ rationale: str | None = None
288
+ error: str | None = None
289
+ history: list[dict[str, Any]] = Field(default_factory=list)
290
+
291
+
292
+ class ApproveRequest(BaseModel):
293
+ confirm: bool = Field(description="Must be true; the user acknowledges the patch is applied to the working tree.")
294
+ run_tests: bool = False
295
+ explicit_permission: bool = False
296
+
297
+
298
+ class TestGenerateRequest(BaseModel):
299
+ finding_id: int
300
+
301
+
302
+ class TestGenerateOut(BaseModel):
303
+ finding_id: int
304
+ configured: bool
305
+ file_path: str | None = None
306
+ content: str | None = None
307
+ error: str | None = None
308
+ note: str = ""
309
+
310
+
311
+ # --------------------------------------------------------------------- architecture
312
+ class ArchitectureNode(BaseModel):
313
+ id: str
314
+ kind: str
315
+ label: str
316
+ files: list[str] = Field(default_factory=list)
317
+ confidence: str = "static"
318
+ details: dict[str, Any] = Field(default_factory=dict)
319
+
320
+
321
+ class ArchitectureEdge(BaseModel):
322
+ source: str
323
+ target: str
324
+ kind: str
325
+
326
+
327
+ class ArchitectureOut(BaseModel):
328
+ repository_id: int
329
+ nodes: list[ArchitectureNode]
330
+ edges: list[ArchitectureEdge]
331
+ findings_by_node: dict[str, list[dict[str, Any]]] = Field(default_factory=dict)
332
+ notes: list[str] = Field(default_factory=list)
333
+
334
+
335
+ # ------------------------------------------------------------------- dependencies
336
+ class DependencyOut(BaseModel):
337
+ name: str
338
+ version: str | None = None
339
+ direct: bool = True
340
+ license: str | None = None
341
+ advisories: list[dict[str, Any]] = Field(default_factory=list)
342
+ status: str = "unknown"
343
+
344
+
345
+ class DependenciesOut(BaseModel):
346
+ repository_id: int
347
+ package_manager: str | None = None
348
+ lockfiles: list[str] = Field(default_factory=list)
349
+ dependencies: list[DependencyOut] = Field(default_factory=list)
350
+ notes: list[str] = Field(default_factory=list)
351
+ osv_checked: bool = False
352
+
353
+
354
+ # ----------------------------------------------------------------------- settings
355
+ class AISettingsIn(BaseModel):
356
+ enabled: bool = False
357
+ provider: str = Field(default="openrouter", max_length=60)
358
+ base_url: str = Field(default="https://openrouter.ai/api/v1", max_length=300)
359
+ model: str = Field(default="", max_length=200)
360
+ timeout: int = Field(default=30, ge=1, le=600)
361
+ max_output_tokens: int = Field(default=3000, ge=1, le=16000)
362
+ share_source_context: bool = False
363
+ # Optional. Held in process memory only; never persisted or echoed back.
364
+ # An empty string means "leave the existing key unchanged"; use clear_api_key
365
+ # to remove it.
366
+ api_key: str = Field(default="", max_length=512)
367
+ clear_api_key: bool = False
368
+
369
+
370
+ class AISettingsOut(BaseModel):
371
+ enabled: bool
372
+ provider: str
373
+ base_url: str
374
+ model: str
375
+ timeout: int
376
+ max_output_tokens: int
377
+ share_source_context: bool
378
+ api_key_configured: bool
379
+ source: Literal["environment", "settings", "none"]
380
+
381
+
382
+ class ScanSettingsIn(BaseModel):
383
+ default_scan_profile: str = "standard"
384
+ allow_test_execution: bool = False
385
+ test_image: str = Field(default="python:3.12-slim", max_length=200)
386
+ test_timeout: int = Field(default=300, ge=1, le=7200)
387
+ max_memory_mb: int = Field(default=512, ge=64, le=16384)
388
+ max_cpu_seconds: int = Field(default=60, ge=1, le=3600)
389
+ max_snapshot_mb: int = Field(default=256, ge=1, le=2048)
390
+ max_write_mb: int = Field(default=64, ge=2, le=2048)
391
+ osv_enabled: bool = True
392
+
393
+
394
+ class ScanSettingsOut(ScanSettingsIn):
395
+ pass
396
+
397
+
398
+ class AIConnectionTestOut(BaseModel):
399
+ ok: bool
400
+ configured: bool
401
+ provider: str | None = None
402
+ model: str | None = None
403
+ detail: str
404
+
405
+
406
+ class ConversationOut(BaseModel):
407
+ id: int
408
+ repository_id: int | None = None
409
+ title: str
410
+ created_at: str
411
+ updated_at: str
412
+ message_count: int = 0
413
+
414
+
415
+ class SummaryRequest(BaseModel):
416
+ repository_id: int | None = None
417
+ scan_id: int | None = None
418
+
419
+
420
+ class SummaryOut(BaseModel):
421
+ configured: bool
422
+ provider: str | None = None
423
+ model: str | None = None
424
+ error: str | None = None
425
+ text: str = ""
426
+
427
+
428
+ class ThreatModelRequest(BaseModel):
429
+ repository_id: int
430
+
431
+
432
+ class ThreatModelOut(BaseModel):
433
+ repository_id: int
434
+ configured: bool
435
+ provider: str | None = None
436
+ model: str | None = None
437
+ error: str | None = None
438
+ text: str = ""
439
+ note: str = ""
440
+
441
+
442
+ class HealthOut(BaseModel):
443
+ status: str
444
+ version: str
445
+ database: str
446
+ ai_configured: bool
447
+ repositories: int
448
+ scans: int
449
+
450
+
451
+ class GenericMessage(BaseModel):
452
+ detail: str