diff --git a/.kiro/specs/atlas-metrics-report/tasks.md b/.kiro/specs/atlas-metrics-report/tasks.md index 7bce65f..7f7f4ef 100644 --- a/.kiro/specs/atlas-metrics-report/tasks.md +++ b/.kiro/specs/atlas-metrics-report/tasks.md @@ -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 diff --git a/.kiro/specs/card-api-integration/.config.kiro b/.kiro/specs/card-api-integration/.config.kiro new file mode 100644 index 0000000..b4a7122 --- /dev/null +++ b/.kiro/specs/card-api-integration/.config.kiro @@ -0,0 +1 @@ +{"specId": "0334e0b6-7ae7-4284-95a0-caed55c59af1", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/card-api-integration/requirements.md b/.kiro/specs/card-api-integration/requirements.md new file mode 100644 index 0000000..baae2c3 --- /dev/null +++ b/.kiro/specs/card-api-integration/requirements.md @@ -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: }`. +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. diff --git a/backend/routes/atlas.js b/backend/routes/atlas.js index d9acbb0..db48b28 100644 --- a/backend/routes/atlas.js +++ b/backend/routes/atlas.js @@ -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 { diff --git a/backend/server.js b/backend/server.js index e3f762b..ef97445 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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}`); diff --git a/docs/TeamDeviceLoader_matched_findings.xlsx b/docs/TeamDeviceLoader_matched_findings.xlsx new file mode 100644 index 0000000..6d08213 Binary files /dev/null and b/docs/TeamDeviceLoader_matched_findings.xlsx differ diff --git a/docs/Team_Device Loader.xlsx b/docs/Team_Device Loader.xlsx new file mode 100644 index 0000000..c0ab813 Binary files /dev/null and b/docs/Team_Device Loader.xlsx differ diff --git a/docs/graniteexport.xlsx b/docs/graniteexport.xlsx new file mode 100644 index 0000000..4eb5dbd Binary files /dev/null and b/docs/graniteexport.xlsx differ diff --git a/frontend/src/App.js b/frontend/src/App.js index 181eb4e..b890ef4 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -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' && } {currentPage === 'knowledge-base' && } {currentPage === 'exports' && } + {currentPage === 'jira' && } {currentPage === 'admin' && isAdmin() && } {currentPage === 'admin' && !isAdmin() && (() => { setCurrentPage('home'); return null; })()} diff --git a/frontend/src/components/NavDrawer.js b/frontend/src/components/NavDrawer.js index d92fb3c..7816304 100644 --- a/frontend/src/components/NavDrawer.js +++ b/frontend/src/components/NavDrawer.js @@ -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' };