Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: storing vul report pdf in db #1952

Merged
merged 10 commits into from
Jan 14, 2025
Merged
63 changes: 62 additions & 1 deletion apps/dashboard/src/main/java/com/akto/action/ReportAction.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.akto.action;

import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
Expand All @@ -10,6 +11,11 @@
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import com.akto.dao.testing.sources.TestReportsDao;
import com.akto.dto.testing.sources.TestReports;
import com.mongodb.MongoCommandException;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.Updates;
import org.apache.struts2.ServletActionContext;
import org.bson.types.ObjectId;
import org.json.JSONObject;
Expand All @@ -32,10 +38,45 @@ public class ReportAction extends UserAction {
private String reportUrl;
private String pdf;
private String status;
private boolean firstPollRequest;

private static final LoggerMaker loggerMaker = new LoggerMaker(ReportAction.class);

public String downloadReportPDF() {
if(reportUrl == null || reportUrl.isEmpty()) {
status = "ERROR";
addActionError("Report URL cannot be empty");
return ERROR.toUpperCase();
}

String reportUrlId;
try {
String path = new URL(reportUrl).getPath();
String[] segments = path.split("/");
reportUrlId = segments[segments.length - 1];
} catch (Exception e) {
status = "ERROR";
addActionError("Report URL cannot be empty");
return ERROR.toUpperCase();
}

if(!ObjectId.isValid(reportUrlId)) {
status = "ERROR";
addActionError("Report URL is invalid");
return ERROR.toUpperCase();
}

ObjectId reportUrlIdObj = new ObjectId(reportUrlId);

if(firstPollRequest) {
TestReports testReport = TestReportsDao.instance.findOne(Filters.eq("_id", reportUrlIdObj));
if(testReport != null && (testReport.getPdfReportString() != null && !testReport.getPdfReportString().isEmpty())) {
status = "COMPLETED";
pdf = testReport.getPdfReportString();
return SUCCESS.toUpperCase();
}
}

if (reportId == null) {
// Initiate PDF generation

Expand Down Expand Up @@ -89,6 +130,22 @@ public String downloadReportPDF() {
if (status.equals("COMPLETED")) {
loggerMaker.infoAndAddToDb("Pdf download status for report id - " + reportId + " completed. Attaching pdf in response ", LogDb.DASHBOARD);
pdf = node.get("base64PDF").textValue();
try {
TestReportsDao.instance.updateOne(Filters.eq("_id", reportUrlIdObj), Updates.set(TestReports.PDF_REPORT_STRING, pdf));
} catch(Exception e) {
loggerMaker.errorAndAddToDb("Error: " + e.getMessage() + ", while updating report binary for reportId: " + reportId, LogDb.DASHBOARD);
if (e instanceof MongoCommandException) {
MongoCommandException mongoException = (MongoCommandException) e;
if (mongoException.getCode() == 17420) {
addActionError("The report is too large to save. Please reduce its size and try again.");
} else {
addActionError("A database error occurred while saving the report. Try again later.");
}
} else {
addActionError("An error occurred while updating the report in DB. Please try again.");
}
status = "ERROR";
}
}
} catch (Exception e) {
loggerMaker.errorAndAddToDb(e, "Error while polling pdf download for report id - " + reportId, LogDb.DASHBOARD);
Expand Down Expand Up @@ -146,4 +203,8 @@ public String getStatus() {
public void setStatus(String status) {
this.status = status;
}

public void setFirstPollRequest(boolean firstPollRequest) {
this.firstPollRequest = firstPollRequest;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -425,11 +425,11 @@ export default {
data: {}
})
},
downloadReportPDF(reportId, organizationName, reportDate, reportUrl) {
downloadReportPDF(reportId, organizationName, reportDate, reportUrl, firstPollRequest) {
return request({
url: '/api/downloadReportPDF',
method: 'post',
data: {reportId, organizationName, reportDate, reportUrl}
data: {reportId, organizationName, reportDate, reportUrl, firstPollRequest}
})
},
fetchScript() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable no-loop-func */
import { Box, Divider, Frame, HorizontalStack, Text, TopBar, VerticalStack } from '@shopify/polaris'
import { Box, Button, Divider, Frame, HorizontalStack, Text, TopBar, VerticalStack } from '@shopify/polaris'
import React, { useEffect, useRef } from 'react'
import api from '../api'
import issuesApi from '@/apps/dashboard/pages/issues/api'
Expand Down Expand Up @@ -233,7 +233,7 @@ const VulnerabilityReport = () => {


const handleDownloadPF = async () => {
const WAIT_DURATION = 2500, MAX_RETRIES = 15
const WAIT_DURATION = 5000, MAX_RETRIES = 60
const reportUrl = window.location.href

let pdfError = ""
Expand All @@ -242,20 +242,32 @@ const VulnerabilityReport = () => {

setPdfDownloadEnabled(false)

const progressToastInterval = setInterval(() => {
func.setToast(true, false, "Report PDF generation in progress. Please wait...")
let reportToastInterval = setInterval(() => {
func.setToast(true, false, "Preparing your report. This might take a moment...")
}, 1000)

let generationStarted = false
setTimeout(() => {
clearInterval(reportToastInterval)
generationStarted = true
if(status === "IN_PROGRESS") {
reportToastInterval = setInterval(() => {
func.setToast(true, false, "Report PDF generation in progress. Please wait...")
}, 1000)
}
}, 6000)

try {
// Trigger pdf download
const startDownloadReponse = await api.downloadReportPDF(null, organizationName, currentDate, reportUrl)
const startDownloadReponse = await api.downloadReportPDF(null, organizationName, currentDate, reportUrl, true)
const reportId = startDownloadReponse?.reportId
status = startDownloadReponse?.status
pdf = startDownloadReponse?.pdf

if (reportId !== null && status === "IN_PROGRESS") {
// Poll for PDF completion
for(let i = 0; i < MAX_RETRIES; i++) {
const pdfPollResponse = await api.downloadReportPDF(reportId, organizationName, currentDate, reportUrl)
const pdfPollResponse = await api.downloadReportPDF(reportId, organizationName, currentDate, reportUrl, false)
status = pdfPollResponse?.status

if (status === "COMPLETED") {
Expand All @@ -268,16 +280,22 @@ const VulnerabilityReport = () => {

await func.sleep(WAIT_DURATION)

func.setToast(true, false, "Report PDF generation in progress. Please wait...")
func.setToast(generationStarted, false, "Report PDF generation in progress. Please wait...")

if(i === MAX_RETRIES - 1) {
pdfError = "Failed to download PDF. The size might be too large. Filter out unnecessary issues and try again."
}
}
} else {
pdfError = "Failed to start PDF download"
if(status !== "COMPLETED") {
pdfError = "Failed to start PDF download"
}
}
} catch (err) {
pdfError = err.message
pdfError = err?.response?.data?.actionErrors?.[0] || err.message
}

clearInterval(progressToastInterval)
clearInterval(reportToastInterval)

if (status === "COMPLETED") {
if (pdf === undefined) {
Expand All @@ -300,13 +318,13 @@ const VulnerabilityReport = () => {
link.click();
func.setToast(true, false, "Report PDF downloaded.")
} catch (err) {
pdfError = err.message
pdfError = err?.response?.data?.actionErrors?.[0] || err.message
}
}
}

if (pdfError !== "") {
func.setToast(true, true, `Error while downloading PDF. Please try again. \nError: ${pdfError}`)
func.setToast(true, true, `Error: ${pdfError}`)
}

setPdfDownloadEnabled(true)
Expand All @@ -321,7 +339,9 @@ const VulnerabilityReport = () => {
<Text variant="bodySm">{currentDate}</Text>
</VerticalStack>
<div style={{ display: 'flex', alignItems: 'center' }}>
{/* <Button primary onClick={() => handleDownloadPF()} disabled={!pdfDownloadEnabled}>Download</Button> */}
{
(window.USER_NAME?.toLowerCase()?.includes("@akto.io") || window.ACCOUNT_NAME?.toLowerCase()?.includes("advanced bank")) ? <Button primary onClick={() => handleDownloadPF()} disabled={!pdfDownloadEnabled}>Download</Button> : <></>
}
<img src='/public/white_logo.svg' alt="Logo" className='top-bar-logo' />
</div>
</HorizontalStack>
Expand Down
Loading