| 727 | } |
| 728 | |
| 729 | function parseNucleiOutput(raw: string, context: any): Finding[] { |
| 730 | if (!raw || raw.trim().length === 0) { |
| 731 | context.logger.info('[Nuclei Parser] No output to parse'); |
| 732 | return []; |
| 733 | } |
| 734 | |
| 735 | const lines = raw |
| 736 | .split(/\r?\n/) |
| 737 | .map((line) => line.trim()) |
| 738 | .filter((line) => line.length > 0); |
| 739 | |
| 740 | context.logger.info(`[Nuclei Parser] Processing ${lines.length} lines`); |
| 741 | |
| 742 | const findings: Finding[] = []; |
| 743 | let jsonLineCount = 0; |
| 744 | let skippedStats = 0; |
| 745 | let skippedNonJson = 0; |
| 746 | |
| 747 | for (const line of lines) { |
| 748 | let payload: any = null; |
| 749 | try { |
| 750 | payload = JSON.parse(line); |
| 751 | jsonLineCount++; |
| 752 | } catch { |
| 753 | skippedNonJson++; |
| 754 | continue; |
| 755 | } |
| 756 | |
| 757 | if (!payload || typeof payload !== 'object') { |
| 758 | continue; |
| 759 | } |
| 760 | |
| 761 | // Skip stats lines (they have "duration" and "matched" as a count) |
| 762 | // Real findings have "template-id" or "template" |
| 763 | if (payload.duration || (!payload['template-id'] && !payload.template)) { |
| 764 | skippedStats++; |
| 765 | continue; |
| 766 | } |
| 767 | |
| 768 | const findingCandidate: Finding = { |
| 769 | templateId: payload['template-id'] || payload['template'] || 'unknown', |
| 770 | name: payload.info?.name || payload.name || 'Unknown', |
| 771 | severity: ( |
| 772 | payload.info?.severity || |
| 773 | payload.severity || |
| 774 | 'info' |
| 775 | ).toLowerCase() as Finding['severity'], |
| 776 | tags: Array.isArray(payload.info?.tags) |
| 777 | ? payload.info.tags |
| 778 | : Array.isArray(payload.tags) |
| 779 | ? payload.tags |
| 780 | : [], |
| 781 | matchedAt: payload['matched-at'] || payload.matched || payload.url || payload.host || '', |
| 782 | extractedResults: Array.isArray(payload['extracted-results']) |
| 783 | ? payload['extracted-results'] |
| 784 | : undefined, |
| 785 | request: payload.request, |
| 786 | response: payload.response, |