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
This commit is contained in:
@@ -397,6 +397,7 @@ function extractFinding(f) {
|
||||
|
||||
return {
|
||||
id: String(f.id),
|
||||
hostId: f.host?.hostId || null,
|
||||
title: f.title || '',
|
||||
severity: typeof f.severity === 'number' ? f.severity : parseFloat(f.severity) || 0,
|
||||
vrrGroup: f.vrrGroup || f.severityGroup || '',
|
||||
@@ -782,7 +783,14 @@ function createIvantiFindingsRouter(db, requireAuth) {
|
||||
|
||||
router.use(requireAuth(db));
|
||||
|
||||
// GET / — cached findings with notes merged in
|
||||
/**
|
||||
* GET /api/ivanti/findings
|
||||
*
|
||||
* Return cached Ivanti findings with notes and overrides merged in.
|
||||
*
|
||||
* @returns {Object} 200 - { findings: Array<Object>, lastSync: string|null, overrides: Object }
|
||||
* @returns {Object} 500 - { error: string } on database error
|
||||
*/
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
res.json(await readStateWithNotes(db));
|
||||
@@ -791,7 +799,15 @@ function createIvantiFindingsRouter(db, requireAuth) {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /sync — trigger immediate sync, return fresh state
|
||||
/**
|
||||
* POST /api/ivanti/findings/sync
|
||||
*
|
||||
* Trigger an immediate Ivanti findings sync and return the fresh state.
|
||||
* Requires Admin or Standard_User group.
|
||||
*
|
||||
* @returns {Object} 200 - { findings: Array<Object>, lastSync: string|null, overrides: Object }
|
||||
* @returns {Object} 500 - { error: string } if sync ran but state could not be read
|
||||
*/
|
||||
router.post('/sync', requireGroup('Admin', 'Standard_User'), async (req, res) => {
|
||||
await syncFindings(db);
|
||||
try {
|
||||
@@ -801,7 +817,14 @@ function createIvantiFindingsRouter(db, requireAuth) {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /counts — open vs closed totals for pie chart
|
||||
/**
|
||||
* GET /api/ivanti/findings/counts
|
||||
*
|
||||
* Return open vs closed finding totals for the pie chart.
|
||||
*
|
||||
* @returns {Object} 200 - { open: number, closed: number }
|
||||
* @returns {Object} 500 - { error: string } on database error
|
||||
*/
|
||||
router.get('/counts', async (req, res) => {
|
||||
try {
|
||||
res.json(await readCounts(db));
|
||||
@@ -810,8 +833,15 @@ function createIvantiFindingsRouter(db, requireAuth) {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /counts/history — last snapshot per day, ascending, for the trend chart.
|
||||
// Uses a window function (ROW_NUMBER) to pick the final sync of each calendar day.
|
||||
/**
|
||||
* GET /api/ivanti/findings/counts/history
|
||||
*
|
||||
* Return the last snapshot per day (ascending) for the trend chart.
|
||||
* Uses a ROW_NUMBER window function to pick the final sync of each calendar day.
|
||||
*
|
||||
* @returns {Object} 200 - { history: Array<{ date: string, open_count: number, closed_count: number }> }
|
||||
* @returns {Object} 500 - { error: string } on database error
|
||||
*/
|
||||
router.get('/counts/history', async (req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
@@ -837,7 +867,15 @@ function createIvantiFindingsRouter(db, requireAuth) {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /fp-workflow-counts — FP finding + unique workflow counts (open + closed)
|
||||
/**
|
||||
* GET /api/ivanti/findings/fp-workflow-counts
|
||||
*
|
||||
* Return FP finding counts and unique workflow ID counts (open + closed),
|
||||
* broken down by workflow status.
|
||||
*
|
||||
* @returns {Object} 200 - { findingCounts: Object, findingTotal: number, idCounts: Object, idTotal: number }
|
||||
* @returns {Object} 500 - { error: string } on database error
|
||||
*/
|
||||
router.get('/fp-workflow-counts', async (req, res) => {
|
||||
try {
|
||||
const row = await new Promise((resolve, reject) => {
|
||||
@@ -860,7 +898,20 @@ function createIvantiFindingsRouter(db, requireAuth) {
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /:findingId/override — save or clear a field override (editor/admin only)
|
||||
/**
|
||||
* PUT /api/ivanti/findings/:findingId/override
|
||||
*
|
||||
* Save or clear a field override for a finding. Requires Admin or Standard_User group.
|
||||
* Sending an empty value clears the override (reverts to Ivanti-sourced data).
|
||||
*
|
||||
* @param {string} findingId - The finding identifier (URL param)
|
||||
* @body {string} field - The field to override; must be one of 'hostName', 'dns'
|
||||
* @body {string} [value] - The override value; empty or omitted to clear
|
||||
*
|
||||
* @returns {Object} 200 - { finding_id: string, field: string, value: string|null }
|
||||
* @returns {Object} 400 - { error: string } when field is not in the allowed list
|
||||
* @returns {Object} 500 - { error: string } on database error
|
||||
*/
|
||||
const OVERRIDE_ALLOWED = ['hostName', 'dns'];
|
||||
router.put('/:findingId/override', requireGroup('Admin', 'Standard_User'), (req, res) => {
|
||||
const { findingId } = req.params;
|
||||
@@ -896,7 +947,18 @@ function createIvantiFindingsRouter(db, requireAuth) {
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /:findingId/note — save or update a note (max 255 chars enforced here)
|
||||
/**
|
||||
* PUT /api/ivanti/findings/:findingId/note
|
||||
*
|
||||
* Save or update a note for a finding (max 255 characters).
|
||||
* Requires Admin or Standard_User group.
|
||||
*
|
||||
* @param {string} findingId - The finding identifier (URL param)
|
||||
* @body {string} [note] - The note text (truncated to 255 chars)
|
||||
*
|
||||
* @returns {Object} 200 - { finding_id: string, note: string }
|
||||
* @returns {Object} 500 - { error: string } on database error
|
||||
*/
|
||||
router.put('/:findingId/note', requireGroup('Admin', 'Standard_User'), (req, res) => {
|
||||
const { findingId } = req.params;
|
||||
const note = String(req.body.note || '').slice(0, 255);
|
||||
|
||||
Reference in New Issue
Block a user