Files
guarddog-nexus/guarddog_nexus/web/static/style.css
Marker689 834138368a refactor: вынос хардкода + LLM-анализ finding'ов
## Часть A: Вынос хардкода
- Новый модуль constants.py — все magic strings, лимиты, severity, ключи
  (104 хардкод-значения централизованы)
- Новый модуль queries.py — общие SQL-запросы (build_scan_list_query,
  build_package_list_query, get_dashboard_stats)
  Убрана дупликация между api/*.py и web/routes.py (~90%)

- config.py: добавлены NLP_ENABLED, nexus_timeout, guarddog_binary,
  log_syslog_facility, LLM-переменные
- nexus_client.py: таймауты из конфига, SHA256_CHUNK_SIZE из constants
- scanner.py: error-ключи из constants, GUARDDOG_OUTPUT_FORMAT из constants
- webhooks.py: RELEVANT_WEBHOOK_ACTIONS, METADATA_PATTERNS, ignore-строки
  из constants
- logging_setup.py: конфигурируемый syslog facility, APP_PACKAGE из constants
- main.py: APP_NAME, APP_DESCRIPTION, APP_PACKAGE из constants
- models.py: поле report: JSON | None в Finding для LLM-отчётов
- harvester.py: авто-очистка tmpdir через finally; ERROR_MESSAGE_MAX_LENGTH
  из constants; PACKAGE_EXTENSIONS вместо SUPPORTED_EXTENSIONS (с .gem)
- api/*.py + web/routes.py: используют build_*_query из queries.py,
  константы для лимитов и сортировок
- tests/conftest.py: SEVERITY_WARNING, DEFAULT_ECOSYSTEM из constants

## Часть B: LLM-анализ finding'ов
- llm.py: клиент для OpenAI-совместимых API с промптом security-аналитика
- harvester.py: авто-триггер после flagged scan, сохранение report в БД
- api/findings.py: POST /{id}/analyze — ручной триггер
- web/routes.py: POST /api/v1/findings/{id}/analyze — HTMX-фрагмент
- _llm_report_fragment.html: шаблон фрагмента с вердиктом
- scan_detail.html, package_detail.html: кнопка Analyze with LLM
  (htmx-post, spinner, inline-замена на LLM-отчёт)
- style.css: стили для .llm-report .verdict-safe/suspicious/malicious

## Часть C: Тесты
- 50 тестов, все зелёные
- Линтер чистый
- Тесты используют constants где нужно
2026-05-10 04:37:07 +03:00

483 lines
8.2 KiB
CSS

/* GuardDog Nexus — Web UI styles */
/* Status badges */
.flagged {
color: var(--pico-color-red-400);
font-weight: bold;
}
.clean {
color: var(--pico-color-green-400);
}
.status-pending {
color: var(--pico-color-yellow-400);
}
.status-scanning {
color: var(--pico-color-blue-400);
animation: pulse 1.5s ease-in-out infinite;
}
.status-completed {
color: var(--pico-color-green-400);
}
.status-failed {
color: var(--pico-color-red-400);
}
/* Severity colors */
.severity-WARNING {
color: var(--pico-color-yellow-400);
}
.severity-ERROR {
color: var(--pico-color-red-400);
}
/* Finding cards */
.finding-card {
margin-bottom: 0.5rem;
padding: 0.5rem;
border-left: 3px solid;
transition: opacity 0.2s;
}
.finding-card:hover {
opacity: 0.9;
}
.finding-card.WARNING {
border-left-color: var(--pico-color-yellow-400);
}
.finding-card.ERROR {
border-left-color: var(--pico-color-red-400);
}
.finding-card.INFO {
border-left-color: var(--pico-color-blue-400);
}
/* Tables */
table {
font-size: 0.9rem;
}
/* Nav */
nav {
margin-bottom: 1rem;
}
/* Stats grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
text-align: center;
padding: 1rem;
transition: transform 0.2s, box-shadow 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
/* Severity ratio bar */
.severity-bar {
display: flex;
height: 8px;
border-radius: 4px;
overflow: hidden;
margin-top: 4px;
margin-bottom: 2rem;
}
.severity-bar .bar-error {
background: var(--pico-color-red-400);
transition: width 0.5s ease;
}
.severity-bar .bar-warning {
background: var(--pico-color-yellow-400);
transition: width 0.5s ease;
}
.severity-bar-labels {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
margin-top: 2px;
margin-bottom: 2rem;
}
/* Scan activity heatmap */
.heatmap {
display: flex;
align-items: flex-end;
gap: 2px;
height: 40px;
margin-top: 4px;
margin-bottom: 2rem;
}
.heatmap-day {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
position: relative;
}
.heatmap-day .bar {
border-radius: 2px 2px 0 0;
opacity: 0.8;
transition: height 0.3s ease, opacity 0.2s;
cursor: default;
}
.heatmap-day:hover .bar {
opacity: 1;
}
.heatmap-day .tooltip {
display: none;
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: var(--pico-color-gray-700);
color: var(--pico-color-white);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.7rem;
white-space: nowrap;
z-index: 10;
margin-bottom: 4px;
}
.heatmap-day:hover .tooltip {
display: block;
}
/* Top rules bar chart */
.top-rules-chart {
margin-bottom: 2rem;
}
.top-rules-chart .rule-bar-row {
display: flex;
align-items: center;
margin-bottom: 0.4rem;
}
.top-rules-chart .rule-name {
flex: 0 0 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: right;
padding-right: 0.75rem;
font-size: 0.85rem;
}
.top-rules-chart .rule-bar-container {
flex: 1;
background: var(--pico-color-gray-500);
border-radius: 4px;
overflow: hidden;
height: 20px;
}
.top-rules-chart .rule-bar {
height: 100%;
background: var(--pico-color-blue-400);
border-radius: 4px;
transition: width 0.5s ease;
min-width: 2px;
}
.top-rules-chart .rule-count {
flex: 0 0 50px;
padding-left: 0.5rem;
font-size: 0.85rem;
}
/* Sticky nav */
nav.sticky {
position: sticky;
top: 0;
z-index: 100;
background: var(--pico-color-dark);
padding: 0.5rem 0;
border-bottom: 1px solid var(--pico-color-gray-500);
}
/* Breadcrumbs */
.breadcrumbs {
margin-bottom: 1rem;
font-size: 0.85rem;
opacity: 0.7;
}
.breadcrumbs a {
text-decoration: none;
}
.breadcrumbs .separator {
margin: 0 0.5rem;
opacity: 0.5;
}
/* Empty states */
.empty-state {
text-align: center;
padding: 2rem 1rem;
opacity: 0.5;
font-style: italic;
}
/* Filter bar */
.filter-bar {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
align-items: center;
}
.filter-bar input[type="text"],
.filter-bar select {
margin-bottom: 0;
}
.filter-bar .filter-btn {
margin-bottom: 0;
}
/* Sortable columns */
th.sortable {
cursor: pointer;
user-select: none;
}
th.sortable:hover {
background: var(--pico-color-gray-600);
}
th.sortable .sort-icon {
margin-left: 0.25rem;
opacity: 0.3;
}
th.sortable.active .sort-icon {
opacity: 1;
}
/* Collapsible findings */
.finding-details {
margin-top: 0.5rem;
}
.finding-details pre {
background: var(--pico-color-gray-700);
padding: 0.5rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.8rem;
}
/* Copy button */
.copy-btn {
cursor: pointer;
background: none;
border: 1px solid var(--pico-color-gray-500);
padding: 0.15rem 0.5rem;
font-size: 0.7rem;
border-radius: 3px;
color: var(--pico-color-gray-300);
transition: background 0.2s;
}
.copy-btn:hover {
background: var(--pico-color-gray-600);
}
.copy-btn.copied {
color: var(--pico-color-green-400);
border-color: var(--pico-color-green-400);
}
/* Spinner for scanning status */
.spinner {
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid var(--pico-color-blue-400);
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-right: 0.25rem;
vertical-align: middle;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
/* Finding header row */
.finding-header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
/* Finding summary */
.finding-summary {
cursor: pointer;
list-style: none;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0;
}
/* Finding summary hint */
.finding-summary-hint {
margin-left: auto;
font-size: 0.8rem;
opacity: 0.5;
}
/* Code block toolbar */
.code-toolbar {
display: flex;
justify-content: flex-end;
margin-bottom: 0.25rem;
}
/* LLM report */
.llm-report {
margin-top: 0.75rem;
padding: 0.6rem 0.8rem;
background: var(--pico-color-gray-700);
border-radius: 6px;
font-size: 0.85rem;
line-height: 1.5;
border-left: 3px solid var(--pico-color-blue-400);
}
.llm-report strong {
color: var(--pico-color-blue-300);
}
.verdict-safe {
color: var(--pico-color-green-400);
font-weight: bold;
}
.verdict-suspicious {
color: var(--pico-color-yellow-400);
font-weight: bold;
}
.verdict-malicious {
color: var(--pico-color-red-400);
font-weight: bold;
}
.llm-actions {
margin-top: 0.5rem;
}
.llm-actions button {
font-size: 0.8rem;
}
/* htmx indicator */
.htmx-indicator {
display: inline;
}
.toggle-all-btn {
font-size: 0.8rem;
margin-bottom: 0.5rem;
cursor: pointer;
background: none;
border: 1px solid var(--pico-color-gray-500);
padding: 0.2rem 0.6rem;
border-radius: 3px;
color: var(--pico-color-gray-300);
}
.toggle-all-btn:hover {
background: var(--pico-color-gray-600);
}
/* Responsive */
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.filter-bar {
flex-direction: column;
align-items: stretch;
}
.top-rules-chart .rule-name {
flex: 0 0 100px;
}
nav ul {
flex-wrap: wrap;
}
table {
font-size: 0.8rem;
}
th, td {
padding: 0.35rem 0.5rem;
}
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr;
}
.stat-card {
padding: 0.75rem;
}
}
/* Print styles */
@media print {
nav, .filter-bar, .copy-btn, .toggle-all-btn, nav.sticky {
display: none !important;
}
body {
background: white;
color: black;
}
}