Add CARD API integration spec, Atlas metrics updates, NavDrawer and server.js cleanup, reference docs

This commit is contained in:
root
2026-04-28 16:38:18 +00:00
parent b1069b1a05
commit caa1d539cc
10 changed files with 176 additions and 230 deletions

View File

@@ -125,7 +125,7 @@ Add a tab system to the Metric Graphs panel on the ReportingPage, with an "Ivant
- Verify `getStatusColor` returns `#10B981` for "active", `#EF4444` for "expired", `#0EA5E9` for "completed", `#64748B` for any other string
- **Validates: Requirements 5.2**
- [~]* 5.8 Write unit tests for Atlas donut components
- [ ]* 5.8 Write unit tests for Atlas donut components
- Test Coverage donut empty state message when totalHosts is 0
- Test Plan type donut empty state message when totalPlans is 0
- Test Plan status donut empty state message when totalPlans is 0

View File

@@ -0,0 +1 @@
{"specId": "0334e0b6-7ae7-4284-95a0-caed55c59af1", "workflowType": "requirements-first", "specType": "feature"}

View File

@@ -0,0 +1,163 @@
# Requirements Document
## Introduction
This feature integrates the CARD API into the STEAM Security Dashboard so that CARD workflow items in the Ivanti Queue can trigger real actions — confirm, decline, redirect, and search — via the CARD API. The integration covers OAuth token management, a backend helper module with automatic update_token handling, specific proxy routes for each CARD operation, and frontend UI updates that let users execute CARD actions directly from the queue. A standalone asset search capability supports Granite ID lookups when assets are reassigned.
## Glossary
- **Dashboard**: The STEAM Security Dashboard — the self-hosted vulnerability management application this feature extends.
- **CARD_API**: The external CARD REST API hosted at `card.charter.com` (production) or `card.caas.stage.charterlab.com` (UAT), authenticated via OAuth Bearer tokens. Read endpoints use the `/api/v1/` path prefix; mutation endpoints use the `/api/v2/` path prefix.
- **CARD_Helper**: The new `backend/helpers/cardApi.js` module responsible for CARD API authentication, token management, and HTTP request execution.
- **Token_Manager**: The component within CARD_Helper that handles OAuth token acquisition via Basic Auth, in-memory caching, and automatic refresh before expiry. Tokens have a one-hour TTL.
- **Queue_Item**: A row in the `ivanti_todo_queue` table with `workflow_type = 'CARD'`, representing a finding staged for CARD action.
- **CARD_Route**: The new Express route module at `backend/routes/cardApi.js` that exposes CARD API operations to the frontend through the backend.
- **Audit_Logger**: The existing `logAudit(db, {...})` helper that records state-changing actions to the `audit_logs` table.
- **Auth_Middleware**: The existing `requireAuth(db)` and `requireGroup(...)` middleware that enforces session validation and role-based access.
- **Asset_ID**: A CARD asset identifier in IPN format (e.g., `98.8.142.56-NATL`). Used as the path parameter in owner lookup and mutation endpoints.
- **Update_Token**: A server-generated token returned by the GET owner endpoint. The update_token is mandatory for all mutation calls (confirm, decline, redirect) and ensures optimistic concurrency control.
- **Disposition**: The ownership state of an asset in CARD. Valid values are `confirmed`, `unconfirmed`, `declined`, and `candidate`.
- **Team**: A CARD team name (e.g., `NTS-AEO-STEAM`). Teams are the organizational unit for asset ownership in CARD.
- **Owner_Record**: The JSON object returned by the GET owner endpoint, containing the asset ownership details, disposition states with team names, scores, timestamps, and the update_token field.
## Requirements
### Requirement 1: CARD API Helper Module
**User Story:** As a backend developer, I want a dedicated CARD API helper module that follows the existing atlasApi.js pattern, so that all CARD API communication is centralized and consistent with the codebase.
#### Acceptance Criteria
1. THE CARD_Helper SHALL export an `isConfigured` boolean that is `true` only when all required environment variables (`CARD_API_URL`, `CARD_API_USER`, `CARD_API_PASS`) are present and non-empty.
2. WHEN `isConfigured` is `false`, THE CARD_Helper SHALL log a warning at module load listing the missing environment variables with the prefix `[card-api]`.
3. THE CARD_Helper SHALL use the Node.js built-in `https` module for all HTTP requests to the CARD_API.
4. THE CARD_Helper SHALL export convenience wrapper functions for GET and POST HTTP methods, each accepting a URL path, optional request body, and optional options object.
5. THE CARD_Helper SHALL set `rejectUnauthorized` to `false` on HTTPS requests when the `CARD_SKIP_TLS` environment variable is set to `'true'`.
6. THE CARD_Helper SHALL apply a configurable request timeout defaulting to 15000 milliseconds.
7. THE CARD_Helper SHALL return a Promise that resolves with an object containing `status` (HTTP status code) and `body` (response body string) for each request.
8. THE CARD_Helper SHALL route read requests (GET) through the `/api/v1/` path prefix and mutation requests (POST) through the `/api/v2/` path prefix, matching the CARD_API versioning scheme.
### Requirement 2: OAuth Token Management
**User Story:** As a backend developer, I want the CARD helper to manage OAuth Bearer tokens automatically, so that downstream code does not need to handle authentication directly.
#### Acceptance Criteria
1. WHEN a CARD API request is made and no cached token exists, THE Token_Manager SHALL acquire a new token by sending a request to the CARD_API `/api/v1/auth/get_token` endpoint with a Basic Auth header containing the base64-encoded `CARD_API_USER:CARD_API_PASS` credentials.
2. WHEN a valid token is received, THE Token_Manager SHALL cache the token in memory along with its expiry timestamp (one-hour TTL from acquisition time).
3. WHEN a cached token exists and its expiry timestamp is more than 60 seconds in the future, THE Token_Manager SHALL reuse the cached token for subsequent requests.
4. WHEN a cached token exists and its expiry timestamp is 60 seconds or less in the future, THE Token_Manager SHALL acquire a new token before making the API request.
5. THE Token_Manager SHALL include the cached Bearer token in the `Authorization` header of all non-authentication CARD API requests.
6. IF the CARD_API returns an HTTP 401 response on a non-authentication request, THEN THE Token_Manager SHALL invalidate the cached token, acquire a new token, and retry the original request exactly once.
7. IF the token acquisition request fails or returns a non-success HTTP status, THEN THE Token_Manager SHALL reject the Promise with a descriptive error message including the HTTP status code and the response body.
### Requirement 3: Environment Variable Configuration
**User Story:** As a system administrator, I want CARD API credentials and settings stored in environment variables following the existing pattern, so that configuration is consistent and secrets are not committed to source control.
#### Acceptance Criteria
1. THE Dashboard SHALL read the following environment variables for CARD API configuration: `CARD_API_URL` (base URL), `CARD_API_USER` (service account username), `CARD_API_PASS` (service account password), and `CARD_SKIP_TLS` (TLS verification toggle).
2. THE Dashboard SHALL document all CARD environment variables in `backend/.env.example` with descriptive comments matching the existing documentation style.
3. WHEN any of `CARD_API_URL`, `CARD_API_USER`, or `CARD_API_PASS` is missing or empty, THE CARD_Helper SHALL treat the integration as unconfigured and report `isConfigured` as `false`.
4. THE Dashboard SHALL treat `CARD_SKIP_TLS` as optional, defaulting to `false` when not set.
### Requirement 4: CARD API Proxy Routes
**User Story:** As a dashboard user, I want backend routes that proxy specific CARD API operations, so that the frontend can trigger CARD actions without exposing API credentials to the browser.
#### Acceptance Criteria
1. THE CARD_Route SHALL export a factory function `createCardApiRouter(db, requireAuth)` that returns an Express Router, following the existing route module pattern.
2. THE CARD_Route SHALL protect all endpoints with `requireAuth(db)` for session validation and `requireGroup('Admin', 'Standard_User')` for role-based access.
3. THE CARD_Route SHALL expose a `GET /api/card/status` endpoint that returns `{ configured: boolean }` indicating whether the CARD API integration is configured.
4. THE CARD_Route SHALL expose a `GET /api/card/teams` endpoint that proxies the CARD_API `GET /api/v1/teams` endpoint and returns the list of CARD teams to the client.
5. THE CARD_Route SHALL expose a `GET /api/card/teams/:teamName/assets` endpoint that proxies the CARD_API `GET /api/v1/team/{teamName}/assets` endpoint, accepting `disposition`, `page`, and `page_size` query parameters.
6. WHEN the `page_size` query parameter is not provided on the assets endpoint, THE CARD_Route SHALL default to a page size of 50.
7. THE CARD_Route SHALL expose a `GET /api/card/owner/:assetId` endpoint that proxies the CARD_API `GET /api/v1/owner/{assetId}` endpoint and returns the Owner_Record including disposition states and the update_token.
8. IF `isConfigured` is `false` when a CARD API proxy endpoint is called, THEN THE CARD_Route SHALL return HTTP 503 with `{ error: 'CARD API is not configured.' }`.
9. IF the CARD_API returns an error response, THEN THE CARD_Route SHALL return the CARD_API HTTP status code and a JSON error body containing the upstream error message.
10. THE CARD_Route SHALL be mounted at the `/api/card` path prefix in `server.js`.
### Requirement 5: CARD Asset Mutation Actions
**User Story:** As a dashboard user, I want to confirm, decline, or redirect CARD assets directly from the queue, so that I can process CARD workflow findings without leaving the dashboard.
#### Acceptance Criteria
1. THE CARD_Route SHALL expose a `POST /api/card/queue/:queueItemId/confirm` endpoint that confirms an asset to a specified team via the CARD_API.
2. THE CARD_Route SHALL expose a `POST /api/card/queue/:queueItemId/decline` endpoint that declines an asset from a specified team via the CARD_API.
3. THE CARD_Route SHALL expose a `POST /api/card/queue/:queueItemId/redirect` endpoint that redirects an asset from one team to another team via the CARD_API.
4. WHEN any mutation endpoint is called, THE CARD_Route SHALL verify that the queue item exists, belongs to the requesting user, has `workflow_type = 'CARD'`, and has `status = 'pending'`.
5. IF the queue item does not exist, does not belong to the user, or is not a CARD workflow item, THEN THE CARD_Route SHALL return HTTP 404 with `{ error: 'Queue item not found.' }`.
6. IF the queue item status is not `'pending'`, THEN THE CARD_Route SHALL return HTTP 400 with `{ error: 'Only pending queue items can be executed.' }`.
7. WHEN a mutation endpoint is called, THE CARD_Route SHALL first call `GET /api/v1/owner/{assetId}` to retrieve the current update_token, then use that update_token in the subsequent mutation call, making the two-step flow transparent to the frontend.
8. WHEN the confirm endpoint is called, THE CARD_Route SHALL send `POST /api/v2/owner/{assetId}/confirm?update_token={token}&comment={comment}` with body `{ "name": "TEAM-NAME" }` to the CARD_API.
9. WHEN the decline endpoint is called, THE CARD_Route SHALL send `POST /api/v2/owner/{assetId}/decline?update_token={token}&comment={comment}` with body `{ "name": "TEAM-NAME" }` to the CARD_API.
10. WHEN the redirect endpoint is called, THE CARD_Route SHALL send `POST /api/v2/owner/{assetId}/{fromTeam}/redirect?update_token={token}` with body `{ "name": "TO-TEAM-NAME" }` to the CARD_API, where `fromTeam` is a path parameter and the destination team is in the request body.
11. THE confirm endpoint SHALL accept a request body containing `teamName` (string, required), `comment` (string, optional), and `assetId` (string, required).
12. THE decline endpoint SHALL accept a request body containing `teamName` (string, required), `comment` (string, optional), and `assetId` (string, required).
13. THE redirect endpoint SHALL accept a request body containing `fromTeam` (string, required), `toTeam` (string, required), and `assetId` (string, required).
14. WHEN the CARD_API mutation call succeeds, THE CARD_Route SHALL update the queue item status to `'complete'` and return the CARD_API response to the client.
15. IF the CARD_API mutation call fails, THEN THE CARD_Route SHALL leave the queue item status as `'pending'` and return the error to the client.
### Requirement 6: Frontend CARD Action UI
**User Story:** As a dashboard user, I want specific Confirm, Decline, and Redirect action buttons on CARD queue items, so that I can perform the correct CARD operation for each finding.
#### Acceptance Criteria
1. WHEN a CARD Queue_Item is displayed in the Ivanti Queue panel, THE Dashboard SHALL render three action buttons labeled "Confirm", "Decline", and "Redirect" on pending CARD items.
2. WHEN the user clicks the "Confirm" button, THE Dashboard SHALL display a form with a team selection dropdown (populated from the `/api/card/teams` endpoint) and an optional comment text field, then send a `POST` request to `/api/card/queue/:queueItemId/confirm` with the selected team name, comment, and asset ID.
3. WHEN the user clicks the "Decline" button, THE Dashboard SHALL display a form with a team selection dropdown and an optional comment text field, then send a `POST` request to `/api/card/queue/:queueItemId/decline` with the selected team name, comment, and asset ID.
4. WHEN the user clicks the "Redirect" button, THE Dashboard SHALL display a form with a "From Team" dropdown and a "To Team" dropdown (both populated from the `/api/card/teams` endpoint), then send a `POST` request to `/api/card/queue/:queueItemId/redirect` with the from team, to team, and asset ID.
5. WHILE a CARD action request is in flight, THE Dashboard SHALL disable the action buttons and display a loading indicator on the affected queue item.
6. WHEN the CARD action request succeeds, THE Dashboard SHALL update the queue item status to `'complete'` in the local UI state without requiring a full queue refresh.
7. IF the CARD action request fails, THEN THE Dashboard SHALL display the error message returned by the backend in an inline error indicator on the affected queue item.
8. WHEN the CARD API is not configured (status endpoint returns `configured: false`), THE Dashboard SHALL disable CARD action buttons and display a tooltip indicating the integration is not configured.
9. THE Dashboard SHALL cache the teams list from `/api/card/teams` for the duration of the browser session to avoid redundant API calls.
### Requirement 7: Asset Search UI
**User Story:** As a dashboard user, I want to search CARD for assets by team and disposition, so that I can find Granite IDs when assets get reassigned.
#### Acceptance Criteria
1. THE Dashboard SHALL provide an asset search interface accessible from the Ivanti Queue page.
2. THE asset search interface SHALL include a team selection dropdown (populated from the `/api/card/teams` endpoint) and a disposition filter dropdown with options: `confirmed`, `unconfirmed`, `declined`, `candidate`.
3. WHEN the user initiates a search, THE Dashboard SHALL send a `GET` request to `/api/card/teams/:teamName/assets` with the selected disposition and `page_size=50`.
4. WHEN the first page of results is returned, THE Dashboard SHALL display the total asset count and render the first page of results in a table.
5. WHEN the total asset count exceeds the page size, THE Dashboard SHALL provide pagination controls to navigate through additional pages by sending subsequent requests with incremented `page` parameters.
6. THE asset search results table SHALL display the Asset_ID and any other identifying fields returned by the CARD_API that help the user locate the correct Granite ID.
7. IF the asset search request fails, THEN THE Dashboard SHALL display the error message returned by the backend in the search results area.
### Requirement 8: Error Handling and Resilience
**User Story:** As a dashboard user, I want clear error feedback when CARD API operations fail, so that I can understand what went wrong and take corrective action.
#### Acceptance Criteria
1. IF the CARD_API is unreachable or the request times out, THEN THE CARD_Helper SHALL reject the Promise with an error message that includes the HTTP method, URL path, and failure reason.
2. IF the token acquisition endpoint returns invalid or unparseable JSON, THEN THE Token_Manager SHALL reject the Promise with a descriptive error message indicating a token parse failure.
3. IF the token acquisition endpoint returns HTTP 403, THEN THE CARD_Route SHALL return HTTP 403 with `{ error: 'CARD access denied. The service account may not be onboarded with the CARD team.' }`.
4. IF the token acquisition endpoint returns HTTP 401, THEN THE CARD_Route SHALL return HTTP 401 with `{ error: 'CARD authorization failed. Check service account credentials.' }`.
5. IF the token acquisition endpoint returns HTTP 525, THEN THE CARD_Route SHALL return HTTP 502 with `{ error: 'CARD LDAP error. The service account may not be provisioned correctly.' }`.
6. IF a CARD_API call returns HTTP 401, THEN THE CARD_Route SHALL return HTTP 401 with `{ error: 'CARD token expired or invalid. The request has been retried once automatically.' }`.
7. IF a CARD_API call returns HTTP 403, THEN THE CARD_Route SHALL return HTTP 403 with `{ error: 'Insufficient CARD permissions for this operation.' }`.
8. THE CARD_Route SHALL catch all unhandled errors from CARD_Helper calls and return HTTP 502 with `{ error: 'CARD API request failed.', details: <error message> }`.
9. THE CARD_Route SHALL log all CARD API errors to the server console with the prefix `[card-api]` for consistent log filtering.
10. IF the CARD_Helper is not configured and a proxy endpoint is called, THEN THE CARD_Route SHALL return HTTP 503 with a message indicating which environment variables are missing.
### Requirement 9: Audit Logging for CARD Actions
**User Story:** As an administrator, I want all CARD API actions logged in the audit trail, so that I can review what CARD operations were performed and by whom.
#### Acceptance Criteria
1. WHEN a CARD confirm action is executed through the dashboard, THE Audit_Logger SHALL record an entry with `action: 'card_confirm'`, `entityType: 'ivanti_todo_queue'`, the queue item ID as `entityId`, and `details` containing the asset ID, team name, comment, and CARD_API response status.
2. WHEN a CARD decline action is executed through the dashboard, THE Audit_Logger SHALL record an entry with `action: 'card_decline'`, `entityType: 'ivanti_todo_queue'`, the queue item ID as `entityId`, and `details` containing the asset ID, team name, comment, and CARD_API response status.
3. WHEN a CARD redirect action is executed through the dashboard, THE Audit_Logger SHALL record an entry with `action: 'card_redirect'`, `entityType: 'ivanti_todo_queue'`, the queue item ID as `entityId`, and `details` containing the asset ID, from team, to team, and CARD_API response status.
4. WHEN a CARD asset search is executed through the dashboard, THE Audit_Logger SHALL record an entry with `action: 'card_search'`, `entityType: 'card_asset'`, `entityId` set to the team name, and `details` containing the disposition filter and result count.
5. WHEN a CARD API action fails, THE Audit_Logger SHALL record an entry with `action: 'card_action_failed'`, `entityType: 'ivanti_todo_queue'`, the queue item ID as `entityId`, and `details` containing the action type, asset ID, error message, and CARD_API response status.
6. THE Audit_Logger SHALL record the requesting user's `userId`, `username`, and `ipAddress` on all CARD audit entries.
7. THE Audit_Logger SHALL use fire-and-forget semantics for CARD audit entries, matching the existing audit logging pattern.

View File

@@ -554,6 +554,9 @@ function createAtlasRouter(db, requireAuth) {
try {
const result = await atlasPost('/ivanti-vulnerabilities-by-host', { host_ids }, { timeout: 30000 });
console.log('[Atlas] POST /ivanti-vulnerabilities-by-host status:', result.status, 'body length:', result.body?.length);
console.log('[Atlas] Response preview:', result.body?.substring(0, 500));
if (result.status >= 200 && result.status < 300) {
let body;
try {

View File

@@ -27,6 +27,7 @@ const createIvantiArchiveRouter = require('./routes/ivantiArchive');
const createIvantiFpWorkflowRouter = require('./routes/ivantiFpWorkflow');
const createComplianceRouter = require('./routes/compliance');
const createAtlasRouter = require('./routes/atlas');
const createJiraTicketsRouter = require('./routes/jiraTickets');
const app = express();
const PORT = process.env.PORT || 3001;
@@ -238,6 +239,9 @@ app.use('/api/compliance', createComplianceRouter(db, upload, requireAuth, requi
// Atlas InfoSec action plan routes — proxy CRUD to Atlas API, local cache for badges
app.use('/api/atlas', createAtlasRouter(db, requireAuth));
// Jira ticket routes — local CRUD + Jira REST API integration (lookup, sync, create)
app.use('/api/jira-tickets', createJiraTicketsRouter(db));
// ========== CVE ENDPOINTS ==========
// Get all CVEs with optional filters (authenticated users)
@@ -1185,234 +1189,6 @@ app.get('/api/stats', requireAuth(db), (req, res) => {
});
});
// ========== JIRA TICKET ENDPOINTS ==========
// Get all JIRA tickets (with optional filters)
app.get('/api/jira-tickets', requireAuth(db), (req, res) => {
const { cve_id, vendor, status } = req.query;
let query = 'SELECT * FROM jira_tickets WHERE 1=1';
const params = [];
if (cve_id) {
query += ' AND cve_id = ?';
params.push(cve_id);
}
if (vendor) {
query += ' AND vendor = ?';
params.push(vendor);
}
if (status) {
query += ' AND status = ?';
params.push(status);
}
query += ' ORDER BY created_at DESC';
db.all(query, params, (err, rows) => {
if (err) {
console.error('Error fetching JIRA tickets:', err);
return res.status(500).json({ error: 'Internal server error.' });
}
res.json(rows);
});
});
// Create JIRA ticket
app.post('/api/jira-tickets', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
const { cve_id, vendor, ticket_key, url, summary, status } = req.body;
// Validation
if (!cve_id || !isValidCveId(cve_id)) {
return res.status(400).json({ error: 'Valid CVE ID is required.' });
}
if (!vendor || !isValidVendor(vendor)) {
return res.status(400).json({ error: 'Valid vendor is required.' });
}
if (!ticket_key || typeof ticket_key !== 'string' || ticket_key.trim().length === 0 || ticket_key.length > 50) {
return res.status(400).json({ error: 'Ticket key is required (max 50 chars).' });
}
if (url && (typeof url !== 'string' || url.length > 500)) {
return res.status(400).json({ error: 'URL must be under 500 characters.' });
}
if (summary && (typeof summary !== 'string' || summary.length > 500)) {
return res.status(400).json({ error: 'Summary must be under 500 characters.' });
}
if (status && !VALID_TICKET_STATUSES.includes(status)) {
return res.status(400).json({ error: `Status must be one of: ${VALID_TICKET_STATUSES.join(', ')}` });
}
const ticketStatus = status || 'Open';
const query = `
INSERT INTO jira_tickets (cve_id, vendor, ticket_key, url, summary, status, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?)
`;
db.run(query, [cve_id, vendor, ticket_key.trim(), url || null, summary || null, ticketStatus, req.user.id], function(err) {
if (err) {
console.error('Error creating JIRA ticket:', err);
return res.status(500).json({ error: 'Internal server error.' });
}
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'jira_ticket_create',
entityType: 'jira_ticket',
entityId: this.lastID.toString(),
details: { cve_id, vendor, ticket_key, status: ticketStatus },
ipAddress: req.ip
});
res.status(201).json({
id: this.lastID,
message: 'JIRA ticket created successfully'
});
});
});
// Update JIRA ticket
app.put('/api/jira-tickets/:id', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
const { id } = req.params;
const { ticket_key, url, summary, status } = req.body;
// Validation
if (ticket_key !== undefined && (typeof ticket_key !== 'string' || ticket_key.trim().length === 0 || ticket_key.length > 50)) {
return res.status(400).json({ error: 'Ticket key must be under 50 chars.' });
}
if (url !== undefined && url !== null && (typeof url !== 'string' || url.length > 500)) {
return res.status(400).json({ error: 'URL must be under 500 characters.' });
}
if (summary !== undefined && summary !== null && (typeof summary !== 'string' || summary.length > 500)) {
return res.status(400).json({ error: 'Summary must be under 500 characters.' });
}
if (status !== undefined && !VALID_TICKET_STATUSES.includes(status)) {
return res.status(400).json({ error: `Status must be one of: ${VALID_TICKET_STATUSES.join(', ')}` });
}
// Build dynamic update
const fields = [];
const values = [];
if (ticket_key !== undefined) { fields.push('ticket_key = ?'); values.push(ticket_key.trim()); }
if (url !== undefined) { fields.push('url = ?'); values.push(url); }
if (summary !== undefined) { fields.push('summary = ?'); values.push(summary); }
if (status !== undefined) { fields.push('status = ?'); values.push(status); }
if (fields.length === 0) {
return res.status(400).json({ error: 'No fields to update.' });
}
fields.push('updated_at = CURRENT_TIMESTAMP');
values.push(id);
db.get('SELECT * FROM jira_tickets WHERE id = ?', [id], (err, existing) => {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Internal server error.' });
}
if (!existing) {
return res.status(404).json({ error: 'JIRA ticket not found.' });
}
db.run(`UPDATE jira_tickets SET ${fields.join(', ')} WHERE id = ?`, values, function(updateErr) {
if (updateErr) {
console.error('Error updating JIRA ticket:', updateErr);
return res.status(500).json({ error: 'Internal server error.' });
}
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'jira_ticket_update',
entityType: 'jira_ticket',
entityId: id,
details: { before: existing, changes: req.body },
ipAddress: req.ip
});
res.json({ message: 'JIRA ticket updated successfully', changes: this.changes });
});
});
});
// Delete JIRA ticket
app.delete('/api/jira-tickets/:id', requireAuth(db), requireGroup('Admin', 'Standard_User'), (req, res) => {
const { id } = req.params;
db.get('SELECT * FROM jira_tickets WHERE id = ?', [id], (err, ticket) => {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Internal server error.' });
}
if (!ticket) {
return res.status(404).json({ error: 'JIRA ticket not found.' });
}
// Admin bypasses all delete restrictions
if (req.user.group === 'Admin') {
return performJiraDelete();
}
// Standard_User: ownership check
if (ticket.created_by && ticket.created_by !== req.user.id) {
return res.status(403).json({ error: 'You can only delete resources you created' });
}
// Standard_User: compliance linkage check
const ticketKey = ticket.ticket_key;
db.all(
`SELECT ci.id, ci.extra_json
FROM compliance_items ci
JOIN compliance_uploads cu ON ci.upload_id = cu.id
WHERE ci.status = 'active' AND ci.extra_json LIKE ?`,
[`%${ticketKey}%`],
(compErr, compLinks) => {
// If compliance_items table doesn't exist yet, treat as no linkage
if (compErr && compErr.message && compErr.message.includes('no such table')) {
compLinks = [];
} else if (compErr) {
console.error(compErr);
return res.status(500).json({ error: 'Internal server error.' });
}
const isLinked = (compLinks || []).some(cl => {
const json = cl.extra_json || '';
return json.includes(ticketKey);
});
if (isLinked) {
return res.status(403).json({ error: 'Cannot delete ticket linked to compliance report. Contact an admin.' });
}
return performJiraDelete();
}
);
function performJiraDelete() {
db.run('DELETE FROM jira_tickets WHERE id = ?', [id], function(deleteErr) {
if (deleteErr) {
console.error('Error deleting JIRA ticket:', deleteErr);
return res.status(500).json({ error: 'Internal server error.' });
}
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'jira_ticket_delete',
entityType: 'jira_ticket',
entityId: id,
details: { ticket_key: ticket.ticket_key, cve_id: ticket.cve_id, vendor: ticket.vendor },
ipAddress: req.ip
});
res.json({ message: 'JIRA ticket deleted successfully' });
});
}
});
});
// Start server
app.listen(PORT, () => {
console.log(`CVE API server running on http://${API_HOST}:${PORT}`);

Binary file not shown.

Binary file not shown.

BIN
docs/graniteexport.xlsx Normal file

Binary file not shown.

View File

@@ -13,6 +13,7 @@ import VulnerabilityTriagePage from './components/pages/ReportingPage';
import KnowledgeBasePage from './components/pages/KnowledgeBasePage';
import ExportsPage from './components/pages/ExportsPage';
import CompliancePage from './components/pages/CompliancePage';
import JiraPage from './components/pages/JiraPage';
import AdminPage from './components/pages/AdminPage';
import ArchiveSummaryBar from './components/pages/ArchiveSummaryBar';
import './App.css';
@@ -1043,6 +1044,7 @@ export default function App() {
{currentPage === 'compliance' && <CompliancePage onNavigate={setCurrentPage} />}
{currentPage === 'knowledge-base' && <KnowledgeBasePage />}
{currentPage === 'exports' && <ExportsPage />}
{currentPage === 'jira' && <JiraPage />}
{currentPage === 'admin' && isAdmin() && <AdminPage />}
{currentPage === 'admin' && !isAdmin() && (() => { setCurrentPage('home'); return null; })()}

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { X, Home, BarChart2, BookOpen, Download, ShieldCheck, Settings } from 'lucide-react';
import { X, Home, BarChart2, BookOpen, Download, ShieldCheck, Settings, Ticket } from 'lucide-react';
import { useAuth } from '../contexts/AuthContext';
const NAV_ITEMS = [
@@ -8,6 +8,7 @@ const NAV_ITEMS = [
{ id: 'compliance', label: 'Compliance', icon: ShieldCheck, color: '#14B8A6', description: 'AEO posture & metrics' },
{ id: 'knowledge-base', label: 'Knowledge Base', icon: BookOpen, color: '#10B981', description: 'Articles & documentation' },
{ id: 'exports', label: 'Exports', icon: Download, color: '#8B5CF6', description: 'Export data & reports' },
{ id: 'jira', label: 'Jira Tickets', icon: Ticket, color: '#6366F1', description: 'Jira issue tracking & sync' },
];
const ADMIN_ITEM = { id: 'admin', label: 'Admin Panel', icon: Settings, color: '#EF4444', description: 'User management & audit' };