Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
Add CI/CD pipeline, feedback modal, Atlas qualys_id fallback, and health endpoint
- Rewrite .gitlab-ci.yml with proper stages, blocking tests, staging
environment on dev box, and SSH-based production deploy to 71.85.90.6
- Add POST /api/health endpoint for pipeline verification
- Add POST /atlas/hosts/:hostId/refresh-cache for Atlas cache staleness
- AtlasSlideOutPanel: auto-resolve qualys_id from Atlas vulnerabilities,
prefer qualys_id over active_host_findings_id, retry on failure
- Add FeedbackModal component with bug report button in header and
feature request in UserMenu, creates GitLab issues via /api/feedback
- Fix all frontend test failures (ESM transforms, TextDecoder polyfill,
fast-check resolution, App.test.js boilerplate replacement)
- Fix root package.json test script to run jest
- Add deploy/ directory with staging systemd service and setup script
2026-05-08 12:47:39 -06:00
|
|
|
import { X, Loader, Shield, Plus, Edit3, Check, AlertCircle, ChevronDown, RefreshCw } from 'lucide-react';
|
2026-04-23 22:18:23 +00:00
|
|
|
import AtlasIcon from './AtlasIcon';
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
|
|
|
|
|
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Plan type badge colors
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
const PLAN_TYPE_COLORS = {
|
|
|
|
|
remediation: '#0EA5E9',
|
|
|
|
|
decommission: '#EF4444',
|
|
|
|
|
false_positive: '#F59E0B',
|
|
|
|
|
risk_acceptance: '#A855F7',
|
|
|
|
|
scan_exclusion: '#64748B',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const VALID_PLAN_TYPES = Object.keys(PLAN_TYPE_COLORS);
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Shared inline style constants
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
const ACCENT = '#0EA5E9';
|
|
|
|
|
|
|
|
|
|
const panelStyle = {
|
|
|
|
|
position: 'fixed', top: 0, right: 0, bottom: 0, width: '420px',
|
|
|
|
|
background: '#0A1220',
|
|
|
|
|
borderLeft: '1px solid rgba(14,165,233,0.15)',
|
|
|
|
|
boxShadow: '-8px 0 32px rgba(0,0,0,0.6)',
|
|
|
|
|
zIndex: 41,
|
|
|
|
|
display: 'flex', flexDirection: 'column',
|
|
|
|
|
overflowY: 'auto',
|
|
|
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const backdropStyle = {
|
|
|
|
|
position: 'fixed', inset: 0,
|
|
|
|
|
background: 'rgba(0,0,0,0.4)',
|
|
|
|
|
zIndex: 40,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const headerStyle = {
|
|
|
|
|
padding: '1.25rem 1.25rem 1rem',
|
|
|
|
|
borderBottom: '1px solid rgba(255,255,255,0.06)',
|
|
|
|
|
flexShrink: 0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const sectionTitleStyle = {
|
|
|
|
|
display: 'flex', alignItems: 'center', gap: '0.4rem',
|
|
|
|
|
fontSize: '0.68rem', fontFamily: "'JetBrains Mono', monospace",
|
|
|
|
|
textTransform: 'uppercase', letterSpacing: '0.1em',
|
|
|
|
|
color: '#475569', marginBottom: '0.75rem',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const inputStyle = {
|
|
|
|
|
width: '100%', boxSizing: 'border-box',
|
|
|
|
|
background: 'rgba(14,165,233,0.06)',
|
|
|
|
|
border: '1px solid rgba(14,165,233,0.2)',
|
|
|
|
|
borderRadius: '0.375rem',
|
|
|
|
|
color: '#E2E8F0',
|
|
|
|
|
padding: '0.5rem 0.625rem',
|
|
|
|
|
fontSize: '0.78rem',
|
|
|
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
|
|
|
outline: 'none',
|
|
|
|
|
transition: 'border-color 0.15s',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const labelStyle = {
|
|
|
|
|
display: 'block',
|
|
|
|
|
fontSize: '0.68rem',
|
|
|
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
|
|
|
color: '#94A3B8',
|
|
|
|
|
marginBottom: '0.3rem',
|
|
|
|
|
textTransform: 'uppercase',
|
|
|
|
|
letterSpacing: '0.05em',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const primaryBtnStyle = {
|
|
|
|
|
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '0.4rem',
|
|
|
|
|
padding: '0.5rem 1rem',
|
|
|
|
|
background: 'rgba(14,165,233,0.15)',
|
|
|
|
|
border: '1px solid #0EA5E9',
|
|
|
|
|
borderRadius: '0.375rem',
|
|
|
|
|
color: '#38BDF8',
|
|
|
|
|
fontSize: '0.75rem',
|
|
|
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
transition: 'all 0.15s',
|
|
|
|
|
textTransform: 'uppercase',
|
|
|
|
|
letterSpacing: '0.05em',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Custom dropdown — dark-themed replacement for native <select>
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
function PlanTypeDropdown({ value, onChange }) {
|
|
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
const ref = useRef(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!open) return;
|
|
|
|
|
const handleClick = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
|
|
|
|
|
document.addEventListener('mousedown', handleClick);
|
|
|
|
|
return () => document.removeEventListener('mousedown', handleClick);
|
|
|
|
|
}, [open]);
|
|
|
|
|
|
|
|
|
|
const color = PLAN_TYPE_COLORS[value] || '#94A3B8';
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div ref={ref} style={{ position: 'relative' }}>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setOpen(!open)}
|
|
|
|
|
style={{
|
|
|
|
|
...inputStyle,
|
|
|
|
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
|
|
|
cursor: 'pointer', textAlign: 'left',
|
|
|
|
|
borderColor: open ? 'rgba(14,165,233,0.5)' : 'rgba(14,165,233,0.2)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<span style={{ color, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.03em' }}>
|
|
|
|
|
{value.replace(/_/g, ' ')}
|
|
|
|
|
</span>
|
|
|
|
|
<ChevronDown style={{ width: 14, height: 14, color: '#475569', transform: open ? 'rotate(180deg)' : 'none', transition: 'transform 0.15s' }} />
|
|
|
|
|
</button>
|
|
|
|
|
{open && (
|
|
|
|
|
<div style={{
|
|
|
|
|
position: 'absolute', top: '100%', left: 0, right: 0, marginTop: '4px',
|
|
|
|
|
background: '#0F1A2E',
|
|
|
|
|
border: '1px solid rgba(14,165,233,0.25)',
|
|
|
|
|
borderRadius: '0.375rem',
|
|
|
|
|
boxShadow: '0 8px 24px rgba(0,0,0,0.5)',
|
|
|
|
|
zIndex: 50, overflow: 'hidden',
|
|
|
|
|
}}>
|
|
|
|
|
{VALID_PLAN_TYPES.map(t => {
|
|
|
|
|
const c = PLAN_TYPE_COLORS[t] || '#94A3B8';
|
|
|
|
|
const isSelected = t === value;
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={t}
|
|
|
|
|
onClick={() => { onChange(t); setOpen(false); }}
|
|
|
|
|
style={{
|
|
|
|
|
padding: '0.5rem 0.625rem',
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
background: isSelected ? 'rgba(14,165,233,0.12)' : 'transparent',
|
|
|
|
|
color: c,
|
|
|
|
|
fontSize: '0.78rem',
|
|
|
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
textTransform: 'uppercase',
|
|
|
|
|
letterSpacing: '0.03em',
|
|
|
|
|
transition: 'background 0.1s',
|
|
|
|
|
}}
|
|
|
|
|
onMouseEnter={e => { if (!isSelected) e.currentTarget.style.background = 'rgba(14,165,233,0.06)'; }}
|
|
|
|
|
onMouseLeave={e => { if (!isSelected) e.currentTarget.style.background = 'transparent'; }}
|
|
|
|
|
>
|
|
|
|
|
{t.replace(/_/g, ' ')}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// PlanTypeBadge — colored pill for plan type
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
function PlanTypeBadge({ type }) {
|
|
|
|
|
const color = PLAN_TYPE_COLORS[type] || '#94A3B8';
|
|
|
|
|
return (
|
|
|
|
|
<span style={{
|
|
|
|
|
display: 'inline-flex', alignItems: 'center',
|
|
|
|
|
padding: '0.2rem 0.5rem',
|
|
|
|
|
background: `${color}18`,
|
|
|
|
|
border: `1px solid ${color}50`,
|
|
|
|
|
borderRadius: '0.25rem',
|
|
|
|
|
color,
|
|
|
|
|
fontSize: '0.7rem',
|
|
|
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
textTransform: 'uppercase',
|
|
|
|
|
letterSpacing: '0.03em',
|
|
|
|
|
}}>
|
|
|
|
|
{type.replace(/_/g, ' ')}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// PlanCard — displays a single action plan
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
function PlanCard({ plan, canWrite, onSaveEdit, editingId, onStartEdit, onCancelEdit }) {
|
|
|
|
|
const isEditing = editingId === (plan.action_plan_id || plan.id);
|
|
|
|
|
|
|
|
|
|
const [editForm, setEditForm] = useState({
|
|
|
|
|
commit_date: plan.commit_date || '',
|
|
|
|
|
qualys_id: plan.qualys_id || '',
|
|
|
|
|
active_host_findings_id: plan.active_host_findings_id || '',
|
|
|
|
|
jira_vnr: plan.jira_vnr || '',
|
|
|
|
|
archer_exc: plan.archer_exc || '',
|
|
|
|
|
});
|
|
|
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
|
const [editError, setEditError] = useState(null);
|
|
|
|
|
|
|
|
|
|
const handleSave = async () => {
|
|
|
|
|
setSaving(true);
|
|
|
|
|
setEditError(null);
|
|
|
|
|
try {
|
|
|
|
|
await onSaveEdit(plan.action_plan_id || plan.id, editForm);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setEditError(err.message);
|
|
|
|
|
} finally {
|
|
|
|
|
setSaving(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const isPending = !!plan._localPending;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div style={{
|
|
|
|
|
marginBottom: '0.625rem', padding: '0.625rem 0.75rem',
|
|
|
|
|
background: isPending ? 'rgba(245,158,11,0.06)' : 'rgba(14,165,233,0.04)',
|
|
|
|
|
border: isPending ? '1px solid rgba(245,158,11,0.25)' : '1px solid rgba(14,165,233,0.12)',
|
|
|
|
|
borderRadius: '0.375rem',
|
|
|
|
|
}}>
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.4rem' }}>
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
|
|
|
|
|
<PlanTypeBadge type={plan.plan_type || 'unknown'} />
|
|
|
|
|
{isPending && (
|
|
|
|
|
<span style={{
|
|
|
|
|
display: 'inline-flex', alignItems: 'center', gap: '3px',
|
|
|
|
|
padding: '0.15rem 0.4rem',
|
|
|
|
|
background: 'rgba(245,158,11,0.12)',
|
|
|
|
|
border: '1px solid rgba(245,158,11,0.35)',
|
|
|
|
|
borderRadius: '0.25rem',
|
|
|
|
|
color: '#F59E0B',
|
|
|
|
|
fontSize: '0.6rem',
|
|
|
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
textTransform: 'uppercase',
|
|
|
|
|
letterSpacing: '0.05em',
|
|
|
|
|
}}>
|
|
|
|
|
pending
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
|
|
|
|
|
{plan.status && !isPending && (
|
|
|
|
|
<span style={{
|
|
|
|
|
display: 'inline-flex', alignItems: 'center', gap: '3px',
|
|
|
|
|
padding: '0.15rem 0.4rem',
|
|
|
|
|
background: 'rgba(16,185,129,0.12)',
|
|
|
|
|
border: '1px solid rgba(16,185,129,0.35)',
|
|
|
|
|
borderRadius: '0.25rem',
|
|
|
|
|
color: '#10B981',
|
|
|
|
|
fontSize: '0.6rem',
|
|
|
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
textTransform: 'uppercase',
|
|
|
|
|
letterSpacing: '0.05em',
|
|
|
|
|
}}>
|
|
|
|
|
{plan.status}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{canWrite && !isEditing && !isPending && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => onStartEdit(plan.action_plan_id || plan.id)}
|
|
|
|
|
title="Edit plan"
|
|
|
|
|
style={{
|
|
|
|
|
background: 'none', border: '1px solid rgba(14,165,233,0.15)',
|
|
|
|
|
borderRadius: '0.25rem', padding: '0.2rem',
|
|
|
|
|
cursor: 'pointer', color: '#475569',
|
|
|
|
|
transition: 'all 0.15s', lineHeight: 1,
|
|
|
|
|
}}
|
|
|
|
|
onMouseEnter={e => { e.currentTarget.style.color = ACCENT; e.currentTarget.style.borderColor = 'rgba(14,165,233,0.5)'; }}
|
|
|
|
|
onMouseLeave={e => { e.currentTarget.style.color = '#475569'; e.currentTarget.style.borderColor = 'rgba(14,165,233,0.15)'; }}
|
|
|
|
|
>
|
|
|
|
|
<Edit3 style={{ width: 11, height: 11 }} />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{!isEditing ? (
|
|
|
|
|
<>
|
|
|
|
|
<div style={{ display: 'flex', gap: '0.4rem', marginBottom: '0.25rem' }}>
|
|
|
|
|
<span style={{ fontSize: '0.68rem', color: '#475569', fontFamily: "'JetBrains Mono', monospace", minWidth: '72px' }}>Commit</span>
|
|
|
|
|
<span style={{ fontSize: '0.68rem', color: '#E2E8F0', fontFamily: "'JetBrains Mono', monospace" }}>{plan.commit_date || '—'}</span>
|
|
|
|
|
</div>
|
|
|
|
|
{plan.jira_vnr && (
|
|
|
|
|
<div style={{ display: 'flex', gap: '0.4rem', marginBottom: '0.25rem' }}>
|
|
|
|
|
<span style={{ fontSize: '0.68rem', color: '#475569', fontFamily: "'JetBrains Mono', monospace", minWidth: '72px' }}>VNR</span>
|
|
|
|
|
<span style={{ fontSize: '0.68rem', color: '#E2E8F0', fontFamily: "'JetBrains Mono', monospace" }}>{plan.jira_vnr}</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{plan.archer_exc && (
|
|
|
|
|
<div style={{ display: 'flex', gap: '0.4rem', marginBottom: '0.25rem' }}>
|
|
|
|
|
<span style={{ fontSize: '0.68rem', color: '#475569', fontFamily: "'JetBrains Mono', monospace", minWidth: '72px' }}>EXC</span>
|
|
|
|
|
<span style={{ fontSize: '0.68rem', color: '#E2E8F0', fontFamily: "'JetBrains Mono', monospace" }}>{plan.archer_exc}</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<div style={{ marginTop: '0.5rem' }}>
|
|
|
|
|
<div style={{ marginBottom: '0.5rem' }}>
|
|
|
|
|
<label style={labelStyle}>Commit Date</label>
|
|
|
|
|
<input
|
|
|
|
|
type="date"
|
|
|
|
|
value={editForm.commit_date}
|
|
|
|
|
onChange={e => setEditForm({ ...editForm, commit_date: e.target.value })}
|
|
|
|
|
style={inputStyle}
|
|
|
|
|
onFocus={e => e.target.style.borderColor = 'rgba(14,165,233,0.5)'}
|
|
|
|
|
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.2)'}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ marginBottom: '0.5rem' }}>
|
|
|
|
|
<label style={labelStyle}>Qualys ID</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={editForm.qualys_id}
|
|
|
|
|
onChange={e => setEditForm({ ...editForm, qualys_id: e.target.value })}
|
|
|
|
|
placeholder="Optional"
|
|
|
|
|
style={inputStyle}
|
|
|
|
|
onFocus={e => e.target.style.borderColor = 'rgba(14,165,233,0.5)'}
|
|
|
|
|
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.2)'}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ marginBottom: '0.5rem' }}>
|
|
|
|
|
<label style={labelStyle}>Findings ID</label>
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
value={editForm.active_host_findings_id}
|
|
|
|
|
onChange={e => setEditForm({ ...editForm, active_host_findings_id: e.target.value })}
|
|
|
|
|
placeholder="Optional"
|
|
|
|
|
style={inputStyle}
|
|
|
|
|
onFocus={e => e.target.style.borderColor = 'rgba(14,165,233,0.5)'}
|
|
|
|
|
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.2)'}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ marginBottom: '0.5rem' }}>
|
|
|
|
|
<label style={labelStyle}>Jira VNR</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={editForm.jira_vnr}
|
|
|
|
|
onChange={e => setEditForm({ ...editForm, jira_vnr: e.target.value })}
|
|
|
|
|
placeholder="Optional"
|
|
|
|
|
style={inputStyle}
|
|
|
|
|
onFocus={e => e.target.style.borderColor = 'rgba(14,165,233,0.5)'}
|
|
|
|
|
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.2)'}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ marginBottom: '0.5rem' }}>
|
|
|
|
|
<label style={labelStyle}>Archer EXC</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={editForm.archer_exc}
|
|
|
|
|
onChange={e => setEditForm({ ...editForm, archer_exc: e.target.value })}
|
|
|
|
|
placeholder="Optional"
|
|
|
|
|
style={inputStyle}
|
|
|
|
|
onFocus={e => e.target.style.borderColor = 'rgba(14,165,233,0.5)'}
|
|
|
|
|
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.2)'}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
{editError && (
|
|
|
|
|
<div style={{ display: 'flex', gap: '0.4rem', alignItems: 'center', color: '#F87171', fontSize: '0.72rem', marginBottom: '0.5rem' }}>
|
|
|
|
|
<AlertCircle style={{ width: 12, height: 12, flexShrink: 0 }} />{editError}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleSave}
|
|
|
|
|
disabled={saving}
|
|
|
|
|
style={{ ...primaryBtnStyle, fontSize: '0.68rem', padding: '0.35rem 0.75rem', opacity: saving ? 0.6 : 1 }}
|
|
|
|
|
>
|
|
|
|
|
{saving ? <Loader style={{ width: 12, height: 12, animation: 'spin 1s linear infinite' }} /> : <Check style={{ width: 12, height: 12 }} />}
|
|
|
|
|
Save
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={onCancelEdit}
|
|
|
|
|
style={{
|
|
|
|
|
padding: '0.35rem 0.75rem',
|
|
|
|
|
background: 'transparent',
|
|
|
|
|
border: '1px solid rgba(255,255,255,0.1)',
|
|
|
|
|
borderRadius: '0.375rem',
|
|
|
|
|
color: '#94A3B8',
|
|
|
|
|
fontSize: '0.68rem',
|
|
|
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
transition: 'all 0.15s',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// InactiveSection — collapsible history of overridden/inactive plans
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
function InactiveSection({ plans }) {
|
|
|
|
|
const [expanded, setExpanded] = useState(false);
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ padding: '0.75rem 1.25rem', borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setExpanded(!expanded)}
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex', alignItems: 'center', gap: '0.4rem',
|
|
|
|
|
background: 'none', border: 'none', cursor: 'pointer', padding: 0,
|
|
|
|
|
fontSize: '0.68rem', fontFamily: "'JetBrains Mono', monospace",
|
|
|
|
|
textTransform: 'uppercase', letterSpacing: '0.1em',
|
|
|
|
|
color: '#475569', width: '100%',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<ChevronDown style={{ width: 12, height: 12, transform: expanded ? 'rotate(180deg)' : 'none', transition: 'transform 0.15s' }} />
|
|
|
|
|
History ({plans.length})
|
|
|
|
|
</button>
|
|
|
|
|
{expanded && (
|
|
|
|
|
<div style={{ marginTop: '0.625rem' }}>
|
|
|
|
|
{plans.map((plan, idx) => (
|
|
|
|
|
<div key={plan.action_plan_id || idx} style={{
|
|
|
|
|
marginBottom: '0.5rem', padding: '0.5rem 0.625rem',
|
|
|
|
|
background: 'rgba(100,116,139,0.04)',
|
|
|
|
|
border: '1px solid rgba(100,116,139,0.1)',
|
|
|
|
|
borderRadius: '0.375rem',
|
|
|
|
|
opacity: 0.7,
|
|
|
|
|
}}>
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', marginBottom: '0.25rem' }}>
|
|
|
|
|
<PlanTypeBadge type={plan.plan_type || 'unknown'} />
|
|
|
|
|
<span style={{
|
|
|
|
|
fontSize: '0.6rem', color: '#64748B',
|
|
|
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
|
|
|
textTransform: 'uppercase',
|
|
|
|
|
}}>
|
|
|
|
|
{plan.status || 'inactive'}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ display: 'flex', gap: '0.4rem' }}>
|
|
|
|
|
<span style={{ fontSize: '0.65rem', color: '#475569', fontFamily: "'JetBrains Mono', monospace", minWidth: '60px' }}>Commit</span>
|
|
|
|
|
<span style={{ fontSize: '0.65rem', color: '#94A3B8', fontFamily: "'JetBrains Mono', monospace" }}>{plan.commit_date || '—'}</span>
|
|
|
|
|
</div>
|
|
|
|
|
{plan.created_at && (
|
|
|
|
|
<div style={{ display: 'flex', gap: '0.4rem' }}>
|
|
|
|
|
<span style={{ fontSize: '0.65rem', color: '#475569', fontFamily: "'JetBrains Mono', monospace", minWidth: '60px' }}>Created</span>
|
|
|
|
|
<span style={{ fontSize: '0.65rem', color: '#94A3B8', fontFamily: "'JetBrains Mono', monospace" }}>{plan.created_at.split('T')[0]}</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// AtlasSlideOutPanel — main exported component
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
export default function AtlasSlideOutPanel({ hostId, hostName, findingId, qualysId, onClose, canWrite, onPlanChange }) {
|
|
|
|
|
const [plans, setPlans] = useState([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [error, setError] = useState(null);
|
|
|
|
|
const [editingId, setEditingId] = useState(null);
|
|
|
|
|
|
Add CI/CD pipeline, feedback modal, Atlas qualys_id fallback, and health endpoint
- Rewrite .gitlab-ci.yml with proper stages, blocking tests, staging
environment on dev box, and SSH-based production deploy to 71.85.90.6
- Add POST /api/health endpoint for pipeline verification
- Add POST /atlas/hosts/:hostId/refresh-cache for Atlas cache staleness
- AtlasSlideOutPanel: auto-resolve qualys_id from Atlas vulnerabilities,
prefer qualys_id over active_host_findings_id, retry on failure
- Add FeedbackModal component with bug report button in header and
feature request in UserMenu, creates GitLab issues via /api/feedback
- Fix all frontend test failures (ESM transforms, TextDecoder polyfill,
fast-check resolution, App.test.js boilerplate replacement)
- Fix root package.json test script to run jest
- Add deploy/ directory with staging systemd service and setup script
2026-05-08 12:47:39 -06:00
|
|
|
// Resolved qualys_id from Atlas vulnerabilities lookup
|
|
|
|
|
const [resolvedQualysId, setResolvedQualysId] = useState(qualysId || '');
|
|
|
|
|
const [qualysLoading, setQualysLoading] = useState(false);
|
|
|
|
|
|
|
|
|
|
// Cache refresh state
|
|
|
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
|
const [refreshMsg, setRefreshMsg] = useState(null);
|
|
|
|
|
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
// Create form state — prepopulate qualys_id and findings ID from the clicked finding
|
|
|
|
|
const [showCreate, setShowCreate] = useState(false);
|
|
|
|
|
const [createForm, setCreateForm] = useState({
|
|
|
|
|
plan_type: 'remediation',
|
|
|
|
|
commit_date: '',
|
|
|
|
|
qualys_id: qualysId || '',
|
|
|
|
|
active_host_findings_id: findingId || '',
|
|
|
|
|
jira_vnr: '',
|
|
|
|
|
archer_exc: '',
|
|
|
|
|
});
|
|
|
|
|
const [creating, setCreating] = useState(false);
|
|
|
|
|
const [createError, setCreateError] = useState(null);
|
|
|
|
|
const [successMsg, setSuccessMsg] = useState(null);
|
|
|
|
|
|
Add CI/CD pipeline, feedback modal, Atlas qualys_id fallback, and health endpoint
- Rewrite .gitlab-ci.yml with proper stages, blocking tests, staging
environment on dev box, and SSH-based production deploy to 71.85.90.6
- Add POST /api/health endpoint for pipeline verification
- Add POST /atlas/hosts/:hostId/refresh-cache for Atlas cache staleness
- AtlasSlideOutPanel: auto-resolve qualys_id from Atlas vulnerabilities,
prefer qualys_id over active_host_findings_id, retry on failure
- Add FeedbackModal component with bug report button in header and
feature request in UserMenu, creates GitLab issues via /api/feedback
- Fix all frontend test failures (ESM transforms, TextDecoder polyfill,
fast-check resolution, App.test.js boilerplate replacement)
- Fix root package.json test script to run jest
- Add deploy/ directory with staging systemd service and setup script
2026-05-08 12:47:39 -06:00
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
// Resolve qualys_id from Atlas vulnerabilities for this host+finding
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (qualysId || !hostId || !findingId) return;
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
const resolve = async () => {
|
|
|
|
|
setQualysLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${API_BASE}/atlas/hosts/vulnerabilities`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ host_ids: [hostId] }),
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) return;
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
// Atlas returns { "host_id": [ { qualys_id, title, active_host_findings_id, ... }, ... ] }
|
|
|
|
|
const vulns = data[String(hostId)] || data[hostId] || [];
|
|
|
|
|
if (!Array.isArray(vulns)) return;
|
|
|
|
|
// Find the vuln that matches our finding ID
|
|
|
|
|
const match = vulns.find(v =>
|
|
|
|
|
String(v.active_host_findings_id) === String(findingId) ||
|
|
|
|
|
String(v.id) === String(findingId)
|
|
|
|
|
);
|
|
|
|
|
if (match && !cancelled) {
|
|
|
|
|
const qid = match.qualys_id || match.sourceId || '';
|
|
|
|
|
setResolvedQualysId(qid);
|
|
|
|
|
setCreateForm(prev => ({ ...prev, qualys_id: qid }));
|
|
|
|
|
}
|
|
|
|
|
} catch (_) { /* non-fatal */ }
|
|
|
|
|
finally { if (!cancelled) setQualysLoading(false); }
|
|
|
|
|
};
|
|
|
|
|
resolve();
|
|
|
|
|
return () => { cancelled = true; };
|
|
|
|
|
}, [hostId, findingId, qualysId]);
|
|
|
|
|
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
// Parse Atlas response — handles { active: [...], inactive: [...] } format
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
function parseAtlasPlans(data) {
|
|
|
|
|
if (Array.isArray(data)) return data;
|
|
|
|
|
if (data && typeof data === 'object') {
|
|
|
|
|
const active = Array.isArray(data.active) ? data.active : [];
|
|
|
|
|
const inactive = Array.isArray(data.inactive) ? data.inactive : [];
|
|
|
|
|
return [...active, ...inactive];
|
|
|
|
|
}
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
// Fetch plans
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
const fetchPlans = useCallback(async () => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${API_BASE}/atlas/hosts/${hostId}/action-plans`, { credentials: 'include' });
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const data = await res.json().catch(() => ({}));
|
|
|
|
|
throw new Error(data.error || `Failed to load plans (${res.status})`);
|
|
|
|
|
}
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
const remotePlans = parseAtlasPlans(data);
|
2026-05-05 11:04:53 -06:00
|
|
|
|
|
|
|
|
// If Atlas returns no plans, check local cache for optimistic bulk-create stubs
|
|
|
|
|
if (remotePlans.length === 0) {
|
|
|
|
|
try {
|
|
|
|
|
const cacheRes = await fetch(`${API_BASE}/atlas/status`, { credentials: 'include' });
|
|
|
|
|
if (cacheRes.ok) {
|
|
|
|
|
const cacheData = await cacheRes.json();
|
|
|
|
|
const hostCache = cacheData.find(r => r.host_id === hostId);
|
|
|
|
|
if (hostCache && hostCache.has_action_plan === 1 && hostCache.plans_json) {
|
|
|
|
|
let cachedPlans = [];
|
|
|
|
|
try { cachedPlans = typeof hostCache.plans_json === 'string' ? JSON.parse(hostCache.plans_json) : hostCache.plans_json; } catch (_) {}
|
|
|
|
|
const stubs = cachedPlans
|
|
|
|
|
.filter(p => p.source === 'bulk-create')
|
|
|
|
|
.map((p, i) => ({
|
|
|
|
|
action_plan_id: 'pending-' + hostId + '-' + i,
|
|
|
|
|
plan_type: p.plan_type || 'unknown',
|
|
|
|
|
commit_date: p.commit_date || '',
|
|
|
|
|
status: 'pending',
|
|
|
|
|
_localPending: true,
|
|
|
|
|
created_at: p.created_at || '',
|
|
|
|
|
}));
|
|
|
|
|
if (stubs.length > 0) {
|
|
|
|
|
setPlans(stubs);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (_) { /* ignore cache fallback errors */ }
|
|
|
|
|
}
|
|
|
|
|
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
// Merge: keep local pending plans that aren't yet confirmed by Atlas
|
|
|
|
|
setPlans(prev => {
|
|
|
|
|
const localPending = prev.filter(p => p._localPending);
|
|
|
|
|
const remoteIds = new Set(remotePlans.map(p => p.action_plan_id));
|
|
|
|
|
// Remove local pending plans that now appear in remote (confirmed)
|
|
|
|
|
const stillPending = localPending.filter(p => !remoteIds.has(p.action_plan_id));
|
|
|
|
|
return [...remotePlans, ...stillPending];
|
|
|
|
|
});
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError(err.message);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}, [hostId]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => { fetchPlans(); }, [fetchPlans]);
|
|
|
|
|
|
|
|
|
|
// Clear success message after 3s
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!successMsg) return;
|
|
|
|
|
const t = setTimeout(() => setSuccessMsg(null), 3000);
|
|
|
|
|
return () => clearTimeout(t);
|
|
|
|
|
}, [successMsg]);
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
Add CI/CD pipeline, feedback modal, Atlas qualys_id fallback, and health endpoint
- Rewrite .gitlab-ci.yml with proper stages, blocking tests, staging
environment on dev box, and SSH-based production deploy to 71.85.90.6
- Add POST /api/health endpoint for pipeline verification
- Add POST /atlas/hosts/:hostId/refresh-cache for Atlas cache staleness
- AtlasSlideOutPanel: auto-resolve qualys_id from Atlas vulnerabilities,
prefer qualys_id over active_host_findings_id, retry on failure
- Add FeedbackModal component with bug report button in header and
feature request in UserMenu, creates GitLab issues via /api/feedback
- Fix all frontend test failures (ESM transforms, TextDecoder polyfill,
fast-check resolution, App.test.js boilerplate replacement)
- Fix root package.json test script to run jest
- Add deploy/ directory with staging systemd service and setup script
2026-05-08 12:47:39 -06:00
|
|
|
// Create plan — prefers qualys_id over active_host_findings_id for
|
|
|
|
|
// resilience against Atlas cache staleness.
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
const handleCreate = async () => {
|
|
|
|
|
if (!createForm.commit_date) {
|
|
|
|
|
setCreateError('Commit date is required');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setCreating(true);
|
|
|
|
|
setCreateError(null);
|
|
|
|
|
try {
|
|
|
|
|
const body = {
|
|
|
|
|
plan_type: createForm.plan_type,
|
|
|
|
|
commit_date: createForm.commit_date,
|
|
|
|
|
};
|
Add CI/CD pipeline, feedback modal, Atlas qualys_id fallback, and health endpoint
- Rewrite .gitlab-ci.yml with proper stages, blocking tests, staging
environment on dev box, and SSH-based production deploy to 71.85.90.6
- Add POST /api/health endpoint for pipeline verification
- Add POST /atlas/hosts/:hostId/refresh-cache for Atlas cache staleness
- AtlasSlideOutPanel: auto-resolve qualys_id from Atlas vulnerabilities,
prefer qualys_id over active_host_findings_id, retry on failure
- Add FeedbackModal component with bug report button in header and
feature request in UserMenu, creates GitLab issues via /api/feedback
- Fix all frontend test failures (ESM transforms, TextDecoder polyfill,
fast-check resolution, App.test.js boilerplate replacement)
- Fix root package.json test script to run jest
- Add deploy/ directory with staging systemd service and setup script
2026-05-08 12:47:39 -06:00
|
|
|
|
|
|
|
|
// Prefer qualys_id — it's stable across cache refreshes.
|
|
|
|
|
// Only fall back to active_host_findings_id if no qualys_id is available.
|
|
|
|
|
if (createForm.qualys_id.trim()) {
|
|
|
|
|
body.qualys_id = createForm.qualys_id.trim();
|
|
|
|
|
} else if (createForm.active_host_findings_id) {
|
|
|
|
|
body.active_host_findings_id = Number(createForm.active_host_findings_id);
|
|
|
|
|
}
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
if (createForm.jira_vnr.trim()) body.jira_vnr = createForm.jira_vnr.trim();
|
|
|
|
|
if (createForm.archer_exc.trim()) body.archer_exc = createForm.archer_exc.trim();
|
|
|
|
|
|
Add CI/CD pipeline, feedback modal, Atlas qualys_id fallback, and health endpoint
- Rewrite .gitlab-ci.yml with proper stages, blocking tests, staging
environment on dev box, and SSH-based production deploy to 71.85.90.6
- Add POST /api/health endpoint for pipeline verification
- Add POST /atlas/hosts/:hostId/refresh-cache for Atlas cache staleness
- AtlasSlideOutPanel: auto-resolve qualys_id from Atlas vulnerabilities,
prefer qualys_id over active_host_findings_id, retry on failure
- Add FeedbackModal component with bug report button in header and
feature request in UserMenu, creates GitLab issues via /api/feedback
- Fix all frontend test failures (ESM transforms, TextDecoder polyfill,
fast-check resolution, App.test.js boilerplate replacement)
- Fix root package.json test script to run jest
- Add deploy/ directory with staging systemd service and setup script
2026-05-08 12:47:39 -06:00
|
|
|
let res = await fetch(`${API_BASE}/atlas/hosts/${hostId}/action-plans`, {
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
method: 'PUT',
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
});
|
Add CI/CD pipeline, feedback modal, Atlas qualys_id fallback, and health endpoint
- Rewrite .gitlab-ci.yml with proper stages, blocking tests, staging
environment on dev box, and SSH-based production deploy to 71.85.90.6
- Add POST /api/health endpoint for pipeline verification
- Add POST /atlas/hosts/:hostId/refresh-cache for Atlas cache staleness
- AtlasSlideOutPanel: auto-resolve qualys_id from Atlas vulnerabilities,
prefer qualys_id over active_host_findings_id, retry on failure
- Add FeedbackModal component with bug report button in header and
feature request in UserMenu, creates GitLab issues via /api/feedback
- Fix all frontend test failures (ESM transforms, TextDecoder polyfill,
fast-check resolution, App.test.js boilerplate replacement)
- Fix root package.json test script to run jest
- Add deploy/ directory with staging systemd service and setup script
2026-05-08 12:47:39 -06:00
|
|
|
let data = await res.json().catch(() => ({}));
|
|
|
|
|
|
|
|
|
|
// If the request failed due to finding not found and we used active_host_findings_id,
|
|
|
|
|
// retry with qualys_id if we have one resolved
|
|
|
|
|
if (!res.ok && body.active_host_findings_id && !body.qualys_id && resolvedQualysId) {
|
|
|
|
|
const retryBody = { ...body };
|
|
|
|
|
delete retryBody.active_host_findings_id;
|
|
|
|
|
retryBody.qualys_id = resolvedQualysId;
|
|
|
|
|
|
|
|
|
|
res = await fetch(`${API_BASE}/atlas/hosts/${hostId}/action-plans`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(retryBody),
|
|
|
|
|
});
|
|
|
|
|
data = await res.json().catch(() => ({}));
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
// Update form to use the working qualys_id going forward
|
|
|
|
|
setCreateForm(prev => ({ ...prev, qualys_id: resolvedQualysId }));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!res.ok) throw new Error(data.error || data.detail || `Create failed (${res.status})`);
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
|
|
|
|
|
// Add optimistic local plan immediately — shown as "pending" until sync confirms
|
|
|
|
|
const localPlan = {
|
|
|
|
|
action_plan_id: data.action_plan_id || ('local-' + Date.now()),
|
|
|
|
|
plan_type: body.plan_type,
|
|
|
|
|
commit_date: body.commit_date,
|
|
|
|
|
qualys_id: body.qualys_id || null,
|
|
|
|
|
active_host_findings_id: body.active_host_findings_id || null,
|
|
|
|
|
jira_vnr: body.jira_vnr || null,
|
|
|
|
|
archer_exc: body.archer_exc || null,
|
|
|
|
|
status: 'pending',
|
|
|
|
|
_localPending: true,
|
|
|
|
|
created_at: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
setPlans(prev => [localPlan, ...prev]);
|
|
|
|
|
|
|
|
|
|
// Reset form
|
Add CI/CD pipeline, feedback modal, Atlas qualys_id fallback, and health endpoint
- Rewrite .gitlab-ci.yml with proper stages, blocking tests, staging
environment on dev box, and SSH-based production deploy to 71.85.90.6
- Add POST /api/health endpoint for pipeline verification
- Add POST /atlas/hosts/:hostId/refresh-cache for Atlas cache staleness
- AtlasSlideOutPanel: auto-resolve qualys_id from Atlas vulnerabilities,
prefer qualys_id over active_host_findings_id, retry on failure
- Add FeedbackModal component with bug report button in header and
feature request in UserMenu, creates GitLab issues via /api/feedback
- Fix all frontend test failures (ESM transforms, TextDecoder polyfill,
fast-check resolution, App.test.js boilerplate replacement)
- Fix root package.json test script to run jest
- Add deploy/ directory with staging systemd service and setup script
2026-05-08 12:47:39 -06:00
|
|
|
setCreateForm({ plan_type: 'remediation', commit_date: '', qualys_id: resolvedQualysId || qualysId || '', active_host_findings_id: findingId || '', jira_vnr: '', archer_exc: '' });
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
setShowCreate(false);
|
|
|
|
|
setSuccessMsg('Action plan created');
|
|
|
|
|
if (onPlanChange) onPlanChange();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setCreateError(err.message);
|
|
|
|
|
} finally {
|
|
|
|
|
setCreating(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
Add CI/CD pipeline, feedback modal, Atlas qualys_id fallback, and health endpoint
- Rewrite .gitlab-ci.yml with proper stages, blocking tests, staging
environment on dev box, and SSH-based production deploy to 71.85.90.6
- Add POST /api/health endpoint for pipeline verification
- Add POST /atlas/hosts/:hostId/refresh-cache for Atlas cache staleness
- AtlasSlideOutPanel: auto-resolve qualys_id from Atlas vulnerabilities,
prefer qualys_id over active_host_findings_id, retry on failure
- Add FeedbackModal component with bug report button in header and
feature request in UserMenu, creates GitLab issues via /api/feedback
- Fix all frontend test failures (ESM transforms, TextDecoder polyfill,
fast-check resolution, App.test.js boilerplate replacement)
- Fix root package.json test script to run jest
- Add deploy/ directory with staging systemd service and setup script
2026-05-08 12:47:39 -06:00
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
// Refresh Atlas cache for this host
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
const handleRefreshCache = async () => {
|
|
|
|
|
setRefreshing(true);
|
|
|
|
|
setRefreshMsg(null);
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${API_BASE}/atlas/hosts/${hostId}/refresh-cache`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
});
|
|
|
|
|
const data = await res.json().catch(() => ({}));
|
|
|
|
|
if (!res.ok) throw new Error(data.error || `Refresh failed (${res.status})`);
|
|
|
|
|
setRefreshMsg('Cache refreshed');
|
|
|
|
|
// Re-fetch plans after cache refresh
|
|
|
|
|
await fetchPlans();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setRefreshMsg('Refresh failed: ' + err.message);
|
|
|
|
|
} finally {
|
|
|
|
|
setRefreshing(false);
|
|
|
|
|
setTimeout(() => setRefreshMsg(null), 4000);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
// Edit plan
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
const handleSaveEdit = async (actionPlanId, updates) => {
|
|
|
|
|
const res = await fetch(`${API_BASE}/atlas/hosts/${hostId}/action-plans`, {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ action_plan_id: actionPlanId, updates }),
|
|
|
|
|
});
|
|
|
|
|
const data = await res.json().catch(() => ({}));
|
|
|
|
|
if (!res.ok) throw new Error(data.error || `Update failed (${res.status})`);
|
|
|
|
|
|
|
|
|
|
setEditingId(null);
|
|
|
|
|
setSuccessMsg('Action plan updated');
|
|
|
|
|
await fetchPlans();
|
|
|
|
|
if (onPlanChange) onPlanChange();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
// Render
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{/* Backdrop */}
|
|
|
|
|
<div onClick={onClose} style={backdropStyle} data-testid="atlas-panel-backdrop" />
|
|
|
|
|
|
|
|
|
|
{/* Panel */}
|
|
|
|
|
<div style={panelStyle} data-testid="atlas-slide-out-panel">
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div style={headerStyle}>
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
|
|
|
|
<div style={{ minWidth: 0, flex: 1, marginRight: '0.75rem' }}>
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', marginBottom: '0.3rem' }}>
|
2026-04-23 22:18:23 +00:00
|
|
|
<AtlasIcon style={{ width: 16, height: 16, color: ACCENT, flexShrink: 0 }} />
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
<span style={{ fontSize: '0.9rem', fontWeight: 700, color: '#E2E8F0', wordBreak: 'break-all', lineHeight: 1.3 }}>
|
|
|
|
|
{hostName || 'Unknown Host'}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<span style={{ fontSize: '0.72rem', color: '#475569', fontFamily: "'JetBrains Mono', monospace" }}>
|
|
|
|
|
Host ID: {hostId}
|
|
|
|
|
</span>
|
Add CI/CD pipeline, feedback modal, Atlas qualys_id fallback, and health endpoint
- Rewrite .gitlab-ci.yml with proper stages, blocking tests, staging
environment on dev box, and SSH-based production deploy to 71.85.90.6
- Add POST /api/health endpoint for pipeline verification
- Add POST /atlas/hosts/:hostId/refresh-cache for Atlas cache staleness
- AtlasSlideOutPanel: auto-resolve qualys_id from Atlas vulnerabilities,
prefer qualys_id over active_host_findings_id, retry on failure
- Add FeedbackModal component with bug report button in header and
feature request in UserMenu, creates GitLab issues via /api/feedback
- Fix all frontend test failures (ESM transforms, TextDecoder polyfill,
fast-check resolution, App.test.js boilerplate replacement)
- Fix root package.json test script to run jest
- Add deploy/ directory with staging systemd service and setup script
2026-05-08 12:47:39 -06:00
|
|
|
{canWrite && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleRefreshCache}
|
|
|
|
|
disabled={refreshing}
|
|
|
|
|
title="Refresh Atlas cache for this host"
|
|
|
|
|
style={{
|
|
|
|
|
display: 'inline-flex', alignItems: 'center', gap: '0.3rem',
|
|
|
|
|
marginTop: '0.4rem',
|
|
|
|
|
padding: '0.25rem 0.5rem',
|
|
|
|
|
background: 'rgba(14,165,233,0.06)',
|
|
|
|
|
border: '1px solid rgba(14,165,233,0.15)',
|
|
|
|
|
borderRadius: '0.25rem',
|
|
|
|
|
color: '#64748B',
|
|
|
|
|
fontSize: '0.62rem',
|
|
|
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
|
|
|
cursor: refreshing ? 'wait' : 'pointer',
|
|
|
|
|
opacity: refreshing ? 0.6 : 1,
|
|
|
|
|
transition: 'all 0.15s',
|
|
|
|
|
textTransform: 'uppercase',
|
|
|
|
|
letterSpacing: '0.05em',
|
|
|
|
|
}}
|
|
|
|
|
onMouseEnter={e => { if (!refreshing) { e.currentTarget.style.color = '#38BDF8'; e.currentTarget.style.borderColor = 'rgba(14,165,233,0.4)'; } }}
|
|
|
|
|
onMouseLeave={e => { e.currentTarget.style.color = '#64748B'; e.currentTarget.style.borderColor = 'rgba(14,165,233,0.15)'; }}
|
|
|
|
|
>
|
|
|
|
|
<RefreshCw style={{ width: 10, height: 10, animation: refreshing ? 'spin 1s linear infinite' : 'none' }} />
|
|
|
|
|
{refreshing ? 'Refreshing...' : 'Refresh Cache'}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
{refreshMsg && (
|
|
|
|
|
<span style={{
|
|
|
|
|
display: 'block', marginTop: '0.3rem',
|
|
|
|
|
fontSize: '0.62rem', fontFamily: "'JetBrains Mono', monospace",
|
|
|
|
|
color: refreshMsg.startsWith('Refresh failed') ? '#F87171' : '#10B981',
|
|
|
|
|
}}>
|
|
|
|
|
{refreshMsg}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#475569', flexShrink: 0, padding: '0.25rem' }}
|
|
|
|
|
onMouseEnter={e => e.currentTarget.style.color = '#E2E8F0'}
|
|
|
|
|
onMouseLeave={e => e.currentTarget.style.color = '#475569'}
|
|
|
|
|
data-testid="atlas-panel-close"
|
|
|
|
|
>
|
|
|
|
|
<X style={{ width: 18, height: 18 }} />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Success message */}
|
|
|
|
|
{successMsg && (
|
|
|
|
|
<div style={{
|
|
|
|
|
margin: '0.75rem 1.25rem 0', padding: '0.5rem 0.75rem',
|
|
|
|
|
background: 'rgba(16,185,129,0.1)',
|
|
|
|
|
border: '1px solid rgba(16,185,129,0.3)',
|
|
|
|
|
borderRadius: '0.375rem',
|
|
|
|
|
color: '#10B981', fontSize: '0.75rem',
|
|
|
|
|
display: 'flex', alignItems: 'center', gap: '0.4rem',
|
|
|
|
|
}}>
|
|
|
|
|
<Check style={{ width: 14, height: 14 }} />{successMsg}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Loading */}
|
|
|
|
|
{loading && (
|
|
|
|
|
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '3rem 0' }}>
|
|
|
|
|
<Loader style={{ width: 28, height: 28, color: ACCENT, animation: 'spin 1s linear infinite' }} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Error */}
|
|
|
|
|
{error && !loading && (
|
|
|
|
|
<div style={{ padding: '1.25rem', display: 'flex', flexDirection: 'column', gap: '0.75rem', alignItems: 'center' }}>
|
|
|
|
|
<div style={{ display: 'flex', gap: '0.5rem', color: '#F87171', fontSize: '0.8rem', alignItems: 'center' }}>
|
|
|
|
|
<AlertCircle style={{ width: 16, height: 16, flexShrink: 0 }} />{error}
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
onClick={fetchPlans}
|
|
|
|
|
style={{
|
|
|
|
|
...primaryBtnStyle,
|
|
|
|
|
fontSize: '0.68rem',
|
|
|
|
|
padding: '0.35rem 0.75rem',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Retry
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Plan list */}
|
|
|
|
|
{!loading && !error && (
|
|
|
|
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
|
|
|
|
{/* Section: Active plans */}
|
|
|
|
|
<div style={{ padding: '1rem 1.25rem', borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
|
|
|
|
|
<div style={sectionTitleStyle}>
|
|
|
|
|
<Shield style={{ width: 14, height: 14, color: ACCENT }} />
|
|
|
|
|
Active Plans ({plans.filter(p => p.status === 'active' || p._localPending).length})
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{plans.filter(p => p.status === 'active' || p._localPending).length === 0 && (
|
|
|
|
|
<div style={{ color: '#475569', fontSize: '0.75rem', fontStyle: 'italic' }}>
|
|
|
|
|
No active action plans for this host.
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{plans.filter(p => p.status === 'active' || p._localPending).map((plan, idx) => (
|
|
|
|
|
<PlanCard
|
|
|
|
|
key={plan.action_plan_id || plan.id || idx}
|
|
|
|
|
plan={plan}
|
|
|
|
|
canWrite={canWrite}
|
|
|
|
|
editingId={editingId}
|
|
|
|
|
onStartEdit={setEditingId}
|
|
|
|
|
onCancelEdit={() => setEditingId(null)}
|
|
|
|
|
onSaveEdit={handleSaveEdit}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Section: Inactive plans (history) — collapsible */}
|
|
|
|
|
{plans.filter(p => p.status !== 'active' && !p._localPending).length > 0 && (
|
|
|
|
|
<InactiveSection plans={plans.filter(p => p.status !== 'active' && !p._localPending)} />
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Section: Create form */}
|
|
|
|
|
{canWrite && (
|
|
|
|
|
<div style={{ padding: '1rem 1.25rem' }}>
|
|
|
|
|
{!showCreate ? (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowCreate(true)}
|
|
|
|
|
style={{
|
|
|
|
|
...primaryBtnStyle,
|
|
|
|
|
width: '100%',
|
|
|
|
|
}}
|
|
|
|
|
onMouseEnter={e => { e.currentTarget.style.background = 'rgba(14,165,233,0.25)'; e.currentTarget.style.boxShadow = '0 0 20px rgba(14,165,233,0.25)'; }}
|
|
|
|
|
onMouseLeave={e => { e.currentTarget.style.background = 'rgba(14,165,233,0.15)'; e.currentTarget.style.boxShadow = 'none'; }}
|
|
|
|
|
data-testid="atlas-create-plan-btn"
|
|
|
|
|
>
|
|
|
|
|
<Plus style={{ width: 14, height: 14 }} />
|
|
|
|
|
New Action Plan
|
|
|
|
|
</button>
|
|
|
|
|
) : (
|
|
|
|
|
<div data-testid="atlas-create-form">
|
|
|
|
|
<div style={sectionTitleStyle}>
|
|
|
|
|
<Plus style={{ width: 14, height: 14, color: ACCENT }} />
|
|
|
|
|
Create Action Plan
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Plan type */}
|
|
|
|
|
<div style={{ marginBottom: '0.625rem' }}>
|
|
|
|
|
<label style={labelStyle}>Plan Type</label>
|
|
|
|
|
<PlanTypeDropdown
|
|
|
|
|
value={createForm.plan_type}
|
|
|
|
|
onChange={val => setCreateForm({ ...createForm, plan_type: val })}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Commit date */}
|
|
|
|
|
<div style={{ marginBottom: '0.625rem' }}>
|
|
|
|
|
<label style={labelStyle}>Commit Date *</label>
|
|
|
|
|
<input
|
|
|
|
|
type="date"
|
|
|
|
|
value={createForm.commit_date}
|
|
|
|
|
onChange={e => setCreateForm({ ...createForm, commit_date: e.target.value })}
|
|
|
|
|
style={inputStyle}
|
|
|
|
|
onFocus={e => e.target.style.borderColor = 'rgba(14,165,233,0.5)'}
|
|
|
|
|
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.2)'}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Qualys ID */}
|
|
|
|
|
<div style={{ marginBottom: '0.625rem' }}>
|
Add CI/CD pipeline, feedback modal, Atlas qualys_id fallback, and health endpoint
- Rewrite .gitlab-ci.yml with proper stages, blocking tests, staging
environment on dev box, and SSH-based production deploy to 71.85.90.6
- Add POST /api/health endpoint for pipeline verification
- Add POST /atlas/hosts/:hostId/refresh-cache for Atlas cache staleness
- AtlasSlideOutPanel: auto-resolve qualys_id from Atlas vulnerabilities,
prefer qualys_id over active_host_findings_id, retry on failure
- Add FeedbackModal component with bug report button in header and
feature request in UserMenu, creates GitLab issues via /api/feedback
- Fix all frontend test failures (ESM transforms, TextDecoder polyfill,
fast-check resolution, App.test.js boilerplate replacement)
- Fix root package.json test script to run jest
- Add deploy/ directory with staging systemd service and setup script
2026-05-08 12:47:39 -06:00
|
|
|
<label style={labelStyle}>
|
|
|
|
|
Qualys ID
|
|
|
|
|
{qualysLoading && <span style={{ color: '#475569', marginLeft: '0.4rem', fontStyle: 'italic', textTransform: 'none' }}>(resolving...)</span>}
|
|
|
|
|
{!qualysLoading && resolvedQualysId && !qualysId && <span style={{ color: '#10B981', marginLeft: '0.4rem', textTransform: 'none' }}>(auto-resolved)</span>}
|
|
|
|
|
</label>
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={createForm.qualys_id}
|
|
|
|
|
onChange={e => setCreateForm({ ...createForm, qualys_id: e.target.value })}
|
Add CI/CD pipeline, feedback modal, Atlas qualys_id fallback, and health endpoint
- Rewrite .gitlab-ci.yml with proper stages, blocking tests, staging
environment on dev box, and SSH-based production deploy to 71.85.90.6
- Add POST /api/health endpoint for pipeline verification
- Add POST /atlas/hosts/:hostId/refresh-cache for Atlas cache staleness
- AtlasSlideOutPanel: auto-resolve qualys_id from Atlas vulnerabilities,
prefer qualys_id over active_host_findings_id, retry on failure
- Add FeedbackModal component with bug report button in header and
feature request in UserMenu, creates GitLab issues via /api/feedback
- Fix all frontend test failures (ESM transforms, TextDecoder polyfill,
fast-check resolution, App.test.js boilerplate replacement)
- Fix root package.json test script to run jest
- Add deploy/ directory with staging systemd service and setup script
2026-05-08 12:47:39 -06:00
|
|
|
placeholder="Preferred — stable across cache refreshes"
|
Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.
Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync
Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row
Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00
|
|
|
style={inputStyle}
|
|
|
|
|
onFocus={e => e.target.style.borderColor = 'rgba(14,165,233,0.5)'}
|
|
|
|
|
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.2)'}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Active Host Findings ID */}
|
|
|
|
|
<div style={{ marginBottom: '0.625rem' }}>
|
|
|
|
|
<label style={labelStyle}>Findings ID</label>
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
value={createForm.active_host_findings_id}
|
|
|
|
|
onChange={e => setCreateForm({ ...createForm, active_host_findings_id: e.target.value })}
|
|
|
|
|
placeholder="Optional"
|
|
|
|
|
style={inputStyle}
|
|
|
|
|
onFocus={e => e.target.style.borderColor = 'rgba(14,165,233,0.5)'}
|
|
|
|
|
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.2)'}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Jira VNR */}
|
|
|
|
|
<div style={{ marginBottom: '0.625rem' }}>
|
|
|
|
|
<label style={labelStyle}>Jira VNR</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={createForm.jira_vnr}
|
|
|
|
|
onChange={e => setCreateForm({ ...createForm, jira_vnr: e.target.value })}
|
|
|
|
|
placeholder="Optional"
|
|
|
|
|
style={inputStyle}
|
|
|
|
|
onFocus={e => e.target.style.borderColor = 'rgba(14,165,233,0.5)'}
|
|
|
|
|
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.2)'}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Archer EXC */}
|
|
|
|
|
<div style={{ marginBottom: '0.625rem' }}>
|
|
|
|
|
<label style={labelStyle}>Archer EXC</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={createForm.archer_exc}
|
|
|
|
|
onChange={e => setCreateForm({ ...createForm, archer_exc: e.target.value })}
|
|
|
|
|
placeholder="Optional"
|
|
|
|
|
style={inputStyle}
|
|
|
|
|
onFocus={e => e.target.style.borderColor = 'rgba(14,165,233,0.5)'}
|
|
|
|
|
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.2)'}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Create error */}
|
|
|
|
|
{createError && (
|
|
|
|
|
<div style={{ display: 'flex', gap: '0.4rem', alignItems: 'center', color: '#F87171', fontSize: '0.72rem', marginBottom: '0.625rem' }}>
|
|
|
|
|
<AlertCircle style={{ width: 12, height: 12, flexShrink: 0 }} />{createError}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Buttons */}
|
|
|
|
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleCreate}
|
|
|
|
|
disabled={creating}
|
|
|
|
|
style={{ ...primaryBtnStyle, opacity: creating ? 0.6 : 1 }}
|
|
|
|
|
onMouseEnter={e => { if (!creating) { e.currentTarget.style.background = 'rgba(14,165,233,0.25)'; } }}
|
|
|
|
|
onMouseLeave={e => { e.currentTarget.style.background = 'rgba(14,165,233,0.15)'; }}
|
|
|
|
|
>
|
|
|
|
|
{creating
|
|
|
|
|
? <Loader style={{ width: 14, height: 14, animation: 'spin 1s linear infinite' }} />
|
|
|
|
|
: <Check style={{ width: 14, height: 14 }} />}
|
|
|
|
|
Create
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => { setShowCreate(false); setCreateError(null); }}
|
|
|
|
|
style={{
|
|
|
|
|
padding: '0.5rem 1rem',
|
|
|
|
|
background: 'transparent',
|
|
|
|
|
border: '1px solid rgba(255,255,255,0.1)',
|
|
|
|
|
borderRadius: '0.375rem',
|
|
|
|
|
color: '#94A3B8',
|
|
|
|
|
fontSize: '0.75rem',
|
|
|
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
transition: 'all 0.15s',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|