Update README and Jira UAT test script
This commit is contained in:
52
README.md
52
README.md
@@ -20,6 +20,7 @@ A self-hosted vulnerability management dashboard for the NTS-AEO-STEAM and NTS-A
|
|||||||
- [Compliance — AEO Posture](#compliance--aeo-posture)
|
- [Compliance — AEO Posture](#compliance--aeo-posture)
|
||||||
- [Knowledge Base](#knowledge-base)
|
- [Knowledge Base](#knowledge-base)
|
||||||
- [Exports](#exports)
|
- [Exports](#exports)
|
||||||
|
- [Jira Tickets](#jira-tickets)
|
||||||
- [Archer Risk Acceptance Tickets](#archer-risk-acceptance-tickets)
|
- [Archer Risk Acceptance Tickets](#archer-risk-acceptance-tickets)
|
||||||
- [Admin Panel](#admin-panel)
|
- [Admin Panel](#admin-panel)
|
||||||
- [Scripts](#scripts)
|
- [Scripts](#scripts)
|
||||||
@@ -192,6 +193,20 @@ IVANTI_FIRST_NAME=
|
|||||||
IVANTI_LAST_NAME=
|
IVANTI_LAST_NAME=
|
||||||
# Set to 'true' if your network has SSL inspection / self-signed certs
|
# Set to 'true' if your network has SSL inspection / self-signed certs
|
||||||
IVANTI_SKIP_TLS=false
|
IVANTI_SKIP_TLS=false
|
||||||
|
|
||||||
|
# Jira Data Center REST API (required for Jira Tickets page)
|
||||||
|
# VPN or Charter Network connection required for all Jira instances.
|
||||||
|
# Service accounts use Basic Auth (JIRA_API_USER + JIRA_API_TOKEN).
|
||||||
|
# PATs require ATLSUP approval — set JIRA_AUTH_METHOD=pat to use JIRA_PAT instead.
|
||||||
|
# Rate limits: 1440 requests/day, burst of 60/minute.
|
||||||
|
JIRA_BASE_URL=https://jira.charter.com
|
||||||
|
JIRA_AUTH_METHOD=basic
|
||||||
|
JIRA_API_USER=your-service-account
|
||||||
|
JIRA_API_TOKEN=your-api-token
|
||||||
|
# JIRA_PAT=your-pat-token
|
||||||
|
JIRA_PROJECT_KEY=VULN
|
||||||
|
JIRA_ISSUE_TYPE=Task
|
||||||
|
JIRA_SKIP_TLS=false
|
||||||
```
|
```
|
||||||
|
|
||||||
**`SESSION_SECRET` is required.** The server will exit on startup if it is not set. Generate one with `openssl rand -base64 32`.
|
**`SESSION_SECRET` is required.** The server will exit on startup if it is not set. Generate one with `openssl rand -base64 32`.
|
||||||
@@ -472,6 +487,29 @@ Bulk export tools for reports and data extracts. Available to Admin, Standard_Us
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Jira Tickets
|
||||||
|
|
||||||
|
A dedicated page for managing Jira Data Center tickets linked to CVE/vendor pairs. Accessible from the navigation drawer. Requires a configured Jira API connection (see [Configuration](#configuration)).
|
||||||
|
|
||||||
|
**Ticket list**
|
||||||
|
- View all tracked Jira tickets with status, CVE ID, vendor, summary, and Jira key
|
||||||
|
- Filter by status or search by keyword
|
||||||
|
- Click a Jira key to open the issue in Jira Data Center
|
||||||
|
|
||||||
|
**Jira API operations (Admin/Standard_User)**
|
||||||
|
- **Lookup** — search for any Jira issue by key and view its current status, assignee, and summary
|
||||||
|
- **Create in Jira** — create a new Jira issue directly from the dashboard with project key, issue type, summary, and description; the resulting ticket is automatically linked to a CVE/vendor pair in the local database
|
||||||
|
- **Sync** — refresh a single ticket's status and summary from Jira, or bulk-sync all tracked tickets via JQL search
|
||||||
|
- **Create / Edit / Delete** — manage local ticket records linking Jira keys to CVE/vendor pairs
|
||||||
|
|
||||||
|
**Connection test (Admin)** — verify Jira API credentials and connectivity from the page header.
|
||||||
|
|
||||||
|
**Rate limit monitoring (Admin)** — view current burst and daily rate limit usage against Charter's posted limits (60/minute burst, 1 440/day).
|
||||||
|
|
||||||
|
All Jira API calls are proxied through the backend. Credentials are never exposed to the browser. Rate limits are enforced client-side with inter-request delays (1s for GETs, 2s for writes). See `docs/jira-api-use-cases.md` for the full API compliance summary.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Archer Risk Acceptance Tickets
|
### Archer Risk Acceptance Tickets
|
||||||
|
|
||||||
Track Archer exception tickets (EXC numbers) linked to specific CVE/vendor pairs.
|
Track Archer exception tickets (EXC numbers) linked to specific CVE/vendor pairs.
|
||||||
@@ -578,6 +616,13 @@ All endpoints are prefixed with `/api`. All endpoints except `/api/auth/login` a
|
|||||||
| POST | `/api/jira-tickets` | Admin, Standard_User | Create a JIRA ticket |
|
| POST | `/api/jira-tickets` | Admin, Standard_User | Create a JIRA ticket |
|
||||||
| PUT | `/api/jira-tickets/:id` | Admin, Standard_User | Update a JIRA ticket |
|
| PUT | `/api/jira-tickets/:id` | Admin, Standard_User | Update a JIRA ticket |
|
||||||
| DELETE | `/api/jira-tickets/:id` | Admin, Standard_User | Delete a JIRA ticket (ownership + compliance check for Standard_User) |
|
| DELETE | `/api/jira-tickets/:id` | Admin, Standard_User | Delete a JIRA ticket (ownership + compliance check for Standard_User) |
|
||||||
|
| GET | `/api/jira-tickets/connection-test` | Admin | Test Jira API connectivity and credentials |
|
||||||
|
| GET | `/api/jira-tickets/rate-limit` | Admin | Get current burst and daily rate limit usage |
|
||||||
|
| GET | `/api/jira-tickets/lookup/:issueKey` | Any | Look up a single Jira issue by key |
|
||||||
|
| POST | `/api/jira-tickets/search` | Any | JQL search for Jira issues |
|
||||||
|
| POST | `/api/jira-tickets/create-in-jira` | Admin, Standard_User | Create an issue in Jira and link it locally |
|
||||||
|
| POST | `/api/jira-tickets/sync-all` | Admin | Bulk-sync all tracked tickets via JQL |
|
||||||
|
| POST | `/api/jira-tickets/:id/sync` | Admin, Standard_User | Sync a single ticket's status from Jira |
|
||||||
|
|
||||||
### Ivanti — Host Findings
|
### Ivanti — Host Findings
|
||||||
|
|
||||||
@@ -717,13 +762,15 @@ cve-dashboard/
|
|||||||
│ │ ├── ivantiFindings.js # Ivanti host findings sync, notes, overrides, FP counts
|
│ │ ├── ivantiFindings.js # Ivanti host findings sync, notes, overrides, FP counts
|
||||||
│ │ ├── ivantiTodoQueue.js # Ivanti Queue — personal FP/Archer/CARD staging list
|
│ │ ├── ivantiTodoQueue.js # Ivanti Queue — personal FP/Archer/CARD staging list
|
||||||
│ │ ├── ivantiArchive.js # Finding archive for severity score drift
|
│ │ ├── ivantiArchive.js # Finding archive for severity score drift
|
||||||
|
│ │ ├── jiraTickets.js # Jira ticket CRUD + Jira REST API integration (lookup, sync, create)
|
||||||
│ │ └── compliance.js # AEO compliance upload, diff, device tracking, notes
|
│ │ └── compliance.js # AEO compliance upload, diff, device tracking, notes
|
||||||
│ ├── middleware/
|
│ ├── middleware/
|
||||||
│ │ └── auth.js # requireAuth and requireGroup middleware
|
│ │ └── auth.js # requireAuth and requireGroup middleware
|
||||||
│ ├── helpers/
|
│ ├── helpers/
|
||||||
│ │ ├── auditLog.js # logAudit helper (fire-and-forget)
|
│ │ ├── auditLog.js # logAudit helper (fire-and-forget)
|
||||||
│ │ ├── driftChecker.js # Schema drift detection: compareSchemaToDrift(), loadConfig(), reconcileConfig()
|
│ │ ├── driftChecker.js # Schema drift detection: compareSchemaToDrift(), loadConfig(), reconcileConfig()
|
||||||
│ │ └── ivantiApi.js # Ivanti API HTTP helpers (multipart, JSON, form POST)
|
│ │ ├── ivantiApi.js # Ivanti API HTTP helpers (multipart, JSON, form POST)
|
||||||
|
│ │ └── jiraApi.js # Jira Data Center REST API helpers (Basic/PAT auth, rate limiting)
|
||||||
│ ├── migrations/ # Sequential migration scripts (run manually with node)
|
│ ├── migrations/ # Sequential migration scripts (run manually with node)
|
||||||
│ └── scripts/
|
│ └── scripts/
|
||||||
│ ├── compliance_config.json # Shared parser config (metric_categories, core_cols, skip_sheets)
|
│ ├── compliance_config.json # Shared parser config (metric_categories, core_cols, skip_sheets)
|
||||||
@@ -740,7 +787,7 @@ cve-dashboard/
|
|||||||
│ └── AuthContext.js # Auth state provider (login, logout, group helpers)
|
│ └── AuthContext.js # Auth state provider (login, logout, group helpers)
|
||||||
└── components/
|
└── components/
|
||||||
├── LoginForm.js # Login page
|
├── LoginForm.js # Login page
|
||||||
├── NavDrawer.js # Side navigation drawer (Admin Panel link for Admin group)
|
├── NavDrawer.js # Side navigation drawer (pages + Admin Panel link for Admin group)
|
||||||
├── UserMenu.js # User dropdown in header (shows group badge)
|
├── UserMenu.js # User dropdown in header (shows group badge)
|
||||||
├── CalendarWidget.js # Due-date calendar with Ivanti finding indicators
|
├── CalendarWidget.js # Due-date calendar with Ivanti finding indicators
|
||||||
├── UserManagement.js # Admin user management modal (quick-access from UserMenu)
|
├── UserManagement.js # Admin user management modal (quick-access from UserMenu)
|
||||||
@@ -760,6 +807,7 @@ cve-dashboard/
|
|||||||
├── ComplianceChartsPanel.js # Compliance trend charts
|
├── ComplianceChartsPanel.js # Compliance trend charts
|
||||||
├── IvantiCountsChart.js # Ivanti counts history chart
|
├── IvantiCountsChart.js # Ivanti counts history chart
|
||||||
├── ArchiveSummaryBar.js # Finding archive summary
|
├── ArchiveSummaryBar.js # Finding archive summary
|
||||||
|
├── JiraPage.js # Jira ticket management and Jira API integration
|
||||||
├── KnowledgeBasePage.js # Knowledge base page
|
├── KnowledgeBasePage.js # Knowledge base page
|
||||||
└── ExportsPage.js # Exports page (group-gated)
|
└── ExportsPage.js # Exports page (group-gated)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ function log(level, message, data) {
|
|||||||
console.log(line);
|
console.log(line);
|
||||||
if (data) {
|
if (data) {
|
||||||
const dataStr = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
const dataStr = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
||||||
console.log(' ' + dataStr.split('\n').join('\n '));
|
// Truncate long data to keep logs readable (HTML error pages can be 50KB+)
|
||||||
|
const truncated = dataStr.length > 2000 ? dataStr.substring(0, 2000) + '\n ... [truncated — ' + dataStr.length + ' chars total]' : dataStr;
|
||||||
|
console.log(' ' + truncated.split('\n').join('\n '));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,21 +94,81 @@ async function testCreateIssue() {
|
|||||||
const projectKey = jiraApi.JIRA_PROJECT_KEY;
|
const projectKey = jiraApi.JIRA_PROJECT_KEY;
|
||||||
assert(projectKey, 'JIRA_PROJECT_KEY must be set in .env');
|
assert(projectKey, 'JIRA_PROJECT_KEY must be set in .env');
|
||||||
|
|
||||||
|
// Discover available issue types for this project
|
||||||
|
const projRes = await jiraApi.jiraGet('/rest/api/2/project/' + encodeURIComponent(projectKey));
|
||||||
|
assert(projRes.status === 200, 'Should be able to fetch project metadata. Got HTTP ' + projRes.status + ': ' + (projRes.body || '').substring(0, 300));
|
||||||
|
|
||||||
|
const projData = JSON.parse(projRes.body);
|
||||||
|
const availableTypes = (projData.issueTypes || []).filter(t => !t.subtask);
|
||||||
|
logInfo('Available issue types:', availableTypes.map(t => t.name));
|
||||||
|
|
||||||
|
// Determine which issue type to use: configured type first, then fallback order
|
||||||
|
const configuredType = jiraApi.JIRA_ISSUE_TYPE || 'Task';
|
||||||
|
const fallbackOrder = [configuredType, 'Story', 'Task', 'Bug'];
|
||||||
|
let issueTypeName = null;
|
||||||
|
|
||||||
|
for (const candidate of fallbackOrder) {
|
||||||
|
if (availableTypes.some(t => t.name === candidate)) {
|
||||||
|
issueTypeName = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If none of the preferred types exist, use the first available non-subtask type
|
||||||
|
if (!issueTypeName && availableTypes.length > 0) {
|
||||||
|
issueTypeName = availableTypes[0].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(issueTypeName, 'No usable issue type found in project ' + projectKey);
|
||||||
|
|
||||||
|
if (issueTypeName !== configuredType) {
|
||||||
|
logWarn('Configured JIRA_ISSUE_TYPE "' + configuredType + '" not available — falling back to "' + issueTypeName + '"');
|
||||||
|
}
|
||||||
|
|
||||||
const fields = {
|
const fields = {
|
||||||
project: { key: projectKey },
|
project: { key: projectKey },
|
||||||
summary: `[UAT TEST] STEAM Dashboard - CVE-2025-0001 - TestVendor - ${new Date().toISOString()}`,
|
summary: '[UAT TEST] STEAM Dashboard - CVE-2025-0001 - TestVendor - ' + new Date().toISOString(),
|
||||||
issuetype: { name: jiraApi.JIRA_ISSUE_TYPE || 'Task' },
|
issuetype: { name: issueTypeName },
|
||||||
description: 'Automated UAT test issue created by STEAM Security Dashboard Jira integration test script. This issue can be safely deleted after review.'
|
description: 'UAT test issue created by STEAM Security Dashboard Jira integration test script. This issue can be safely deleted after review.'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Epic type requires an Epic Name field — add it if creating an Epic
|
||||||
|
if (issueTypeName === 'Epic') {
|
||||||
|
fields.customfield_10004 = fields.summary; // Epic Name (standard Jira field ID)
|
||||||
|
}
|
||||||
|
|
||||||
logInfo('Creating issue with fields:', { project: fields.project, summary: fields.summary, issuetype: fields.issuetype });
|
logInfo('Creating issue with fields:', { project: fields.project, summary: fields.summary, issuetype: fields.issuetype });
|
||||||
|
|
||||||
const result = await jiraApi.createIssue(fields);
|
let result = await jiraApi.createIssue(fields);
|
||||||
assert(result.ok, 'Create issue should succeed. Got: ' + JSON.stringify(result));
|
|
||||||
|
// If the first attempt fails with 400, try without description (some screens don't have it)
|
||||||
|
if (!result.ok && result.status === 400) {
|
||||||
|
const errBody = (result.body || '').substring(0, 500);
|
||||||
|
logWarn('Create failed with 400, retrying without description. Error: ' + errBody);
|
||||||
|
|
||||||
|
const retryFields = { ...fields };
|
||||||
|
delete retryFields.description;
|
||||||
|
result = await jiraApi.createIssue(retryFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still failing with 400 and we used Epic, try without the customfield_10004
|
||||||
|
// (Epic Name field ID varies across Jira instances)
|
||||||
|
if (!result.ok && result.status === 400 && issueTypeName === 'Epic') {
|
||||||
|
const errBody = (result.body || '').substring(0, 500);
|
||||||
|
logWarn('Epic create failed, retrying with alternate Epic Name field. Error: ' + errBody);
|
||||||
|
|
||||||
|
const retryFields = { ...fields };
|
||||||
|
delete retryFields.customfield_10004;
|
||||||
|
// Try common alternate Epic Name field IDs
|
||||||
|
retryFields.customfield_10011 = fields.summary;
|
||||||
|
result = await jiraApi.createIssue(retryFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(result.ok, 'Create issue should succeed. Got HTTP ' + result.status + ': ' + (result.body || '').substring(0, 500));
|
||||||
assert(result.data && result.data.key, 'Should return issue key');
|
assert(result.data && result.data.key, 'Should return issue key');
|
||||||
|
|
||||||
createdIssueKey = result.data.key;
|
createdIssueKey = result.data.key;
|
||||||
logInfo('Created issue:', { key: createdIssueKey, id: result.data.id, self: result.data.self });
|
logInfo('Created issue:', { key: createdIssueKey, id: result.data.id, self: result.data.self, issueType: issueTypeName });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -117,7 +179,7 @@ async function testGetIssue() {
|
|||||||
assert(createdIssueKey, 'Need a created issue key from previous test');
|
assert(createdIssueKey, 'Need a created issue key from previous test');
|
||||||
|
|
||||||
const result = await jiraApi.getIssue(createdIssueKey);
|
const result = await jiraApi.getIssue(createdIssueKey);
|
||||||
assert(result.ok, 'Get issue should succeed. Got: ' + JSON.stringify(result));
|
assert(result.ok, 'Get issue should succeed. Got HTTP ' + (result.status || '') + ': ' + ((result.body || '').substring(0, 500)));
|
||||||
|
|
||||||
const issue = result.data;
|
const issue = result.data;
|
||||||
assert(issue.key === createdIssueKey, 'Returned key should match');
|
assert(issue.key === createdIssueKey, 'Returned key should match');
|
||||||
@@ -142,7 +204,7 @@ async function testUpdateIssue() {
|
|||||||
const result = await jiraApi.updateIssue(createdIssueKey, {
|
const result = await jiraApi.updateIssue(createdIssueKey, {
|
||||||
summary: `[UAT TEST] STEAM Dashboard - UPDATED - ${new Date().toISOString()}`
|
summary: `[UAT TEST] STEAM Dashboard - UPDATED - ${new Date().toISOString()}`
|
||||||
});
|
});
|
||||||
assert(result.ok, 'Update issue should succeed (204). Got: ' + JSON.stringify(result));
|
assert(result.ok, 'Update issue should succeed (204). Got HTTP ' + (result.status || '') + ': ' + ((result.body || '').substring(0, 500)));
|
||||||
logInfo('Updated issue summary successfully');
|
logInfo('Updated issue summary successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +218,7 @@ async function testAddComment() {
|
|||||||
const commentBody = `STEAM Dashboard UAT test comment.\nTimestamp: ${new Date().toISOString()}\nThis comment was created by the automated test script.`;
|
const commentBody = `STEAM Dashboard UAT test comment.\nTimestamp: ${new Date().toISOString()}\nThis comment was created by the automated test script.`;
|
||||||
|
|
||||||
const result = await jiraApi.addComment(createdIssueKey, commentBody);
|
const result = await jiraApi.addComment(createdIssueKey, commentBody);
|
||||||
assert(result.ok, 'Add comment should succeed. Got: ' + JSON.stringify(result));
|
assert(result.ok, 'Add comment should succeed. Got HTTP ' + (result.status || '') + ': ' + ((result.body || '').substring(0, 500)));
|
||||||
assert(result.data && result.data.id, 'Should return comment ID');
|
assert(result.data && result.data.id, 'Should return comment ID');
|
||||||
|
|
||||||
logInfo('Added comment:', { commentId: result.data.id });
|
logInfo('Added comment:', { commentId: result.data.id });
|
||||||
@@ -171,7 +233,7 @@ async function testGetTransitions() {
|
|||||||
assert(createdIssueKey, 'Need a created issue key from previous test');
|
assert(createdIssueKey, 'Need a created issue key from previous test');
|
||||||
|
|
||||||
const result = await jiraApi.getTransitions(createdIssueKey);
|
const result = await jiraApi.getTransitions(createdIssueKey);
|
||||||
assert(result.ok, 'Get transitions should succeed. Got: ' + JSON.stringify(result));
|
assert(result.ok, 'Get transitions should succeed. Got HTTP ' + (result.status || '') + ': ' + ((result.body || '').substring(0, 500)));
|
||||||
|
|
||||||
const transitions = result.data.transitions || [];
|
const transitions = result.data.transitions || [];
|
||||||
logInfo('Available transitions:', transitions.map(t => ({ id: t.id, name: t.name, to: t.to ? t.to.name : null })));
|
logInfo('Available transitions:', transitions.map(t => ({ id: t.id, name: t.name, to: t.to ? t.to.name : null })));
|
||||||
@@ -197,7 +259,7 @@ async function testTransitionIssue(transitions) {
|
|||||||
logInfo(`Transitioning to: ${transition.name} (id: ${transition.id})`);
|
logInfo(`Transitioning to: ${transition.name} (id: ${transition.id})`);
|
||||||
|
|
||||||
const result = await jiraApi.transitionIssue(createdIssueKey, transition.id);
|
const result = await jiraApi.transitionIssue(createdIssueKey, transition.id);
|
||||||
assert(result.ok, 'Transition should succeed (204). Got: ' + JSON.stringify(result));
|
assert(result.ok, 'Transition should succeed (204). Got HTTP ' + (result.status || '') + ': ' + ((result.body || '').substring(0, 500)));
|
||||||
logInfo('Transition successful');
|
logInfo('Transition successful');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,11 +272,12 @@ async function testJqlSearch() {
|
|||||||
const projectKey = jiraApi.JIRA_PROJECT_KEY;
|
const projectKey = jiraApi.JIRA_PROJECT_KEY;
|
||||||
assert(projectKey, 'JIRA_PROJECT_KEY must be set');
|
assert(projectKey, 'JIRA_PROJECT_KEY must be set');
|
||||||
|
|
||||||
const jql = `project = ${projectKey} AND updated >= -1h ORDER BY updated DESC`;
|
// Use a broad time window to ensure results even on a quiet project
|
||||||
|
const jql = `project = ${projectKey} ORDER BY updated DESC`;
|
||||||
logInfo('Searching with JQL:', jql);
|
logInfo('Searching with JQL:', jql);
|
||||||
|
|
||||||
const result = await jiraApi.searchIssues(jql, { maxResults: 10 });
|
const result = await jiraApi.searchIssues(jql, { maxResults: 10 });
|
||||||
assert(result.ok, 'Search should succeed. Got: ' + JSON.stringify(result));
|
assert(result.ok, 'Search should succeed. Got HTTP ' + (result.status || '') + ': ' + ((result.body || '').substring(0, 500)));
|
||||||
|
|
||||||
const data = result.data;
|
const data = result.data;
|
||||||
logInfo('Search results:', {
|
logInfo('Search results:', {
|
||||||
@@ -241,7 +304,7 @@ async function testBulkKeySearch() {
|
|||||||
logInfo('Bulk searching keys:', keys);
|
logInfo('Bulk searching keys:', keys);
|
||||||
|
|
||||||
const result = await jiraApi.searchIssuesByKeys(keys);
|
const result = await jiraApi.searchIssuesByKeys(keys);
|
||||||
assert(result.ok, 'Bulk key search should succeed. Got: ' + JSON.stringify(result));
|
assert(result.ok, 'Bulk key search should succeed. Got HTTP ' + (result.status || '') + ': ' + ((result.body || '').substring(0, 500)));
|
||||||
|
|
||||||
const found = (result.data.issues || []).map(i => i.key);
|
const found = (result.data.issues || []).map(i => i.key);
|
||||||
logInfo('Found issues:', found);
|
logInfo('Found issues:', found);
|
||||||
@@ -330,7 +393,9 @@ function writeLog() {
|
|||||||
const lines = results.map(r => {
|
const lines = results.map(r => {
|
||||||
let line = `[${r.timestamp}] ${r.level.toUpperCase().padEnd(5)} ${r.message}`;
|
let line = `[${r.timestamp}] ${r.level.toUpperCase().padEnd(5)} ${r.message}`;
|
||||||
if (r.data) {
|
if (r.data) {
|
||||||
line += '\n ' + (typeof r.data === 'string' ? r.data : JSON.stringify(r.data, null, 2)).split('\n').join('\n ');
|
const dataStr = (typeof r.data === 'string' ? r.data : JSON.stringify(r.data, null, 2));
|
||||||
|
const truncated = dataStr.length > 2000 ? dataStr.substring(0, 2000) + '\n ... [truncated — ' + dataStr.length + ' chars total]' : dataStr;
|
||||||
|
line += '\n ' + truncated.split('\n').join('\n ');
|
||||||
}
|
}
|
||||||
return line;
|
return line;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user