11import axios , { AxiosInstance , AxiosError } from 'axios' ;
2+ import * as https from 'https' ;
23import * as vscode from 'vscode' ;
34import FormData from 'form-data' ;
45import * as fs from 'fs' ;
@@ -106,6 +107,34 @@ export interface LicenseScanResult {
106107 riskLevel : string ;
107108}
108109
110+ /**
111+ * Response from /app/api/ExternalScan — returns all results immediately.
112+ * The project is ephemeral (auto-deleted), so no polling is needed.
113+ */
114+ export interface ExternalScanResponse {
115+ projectId : string ;
116+ status : number ;
117+ vulnerabilities : ExternalScanVuln [ ] | null ;
118+ malwares : any [ ] | null ;
119+ licenses : any [ ] | null ;
120+ dependencyVulnerabilities : any [ ] | null ;
121+ }
122+
123+ export interface ExternalScanVuln {
124+ id : string ;
125+ fileName : string ;
126+ filePath : string ;
127+ lineNumber : string ; // "line,column" format e.g. "2,1"
128+ codeSnippet : string ; // base64-encoded
129+ type : string ;
130+ riskLevel : number ;
131+ vulnerability : string ;
132+ title : string ;
133+ effect : string ;
134+ references : string ;
135+ recommendation : string ;
136+ }
137+
109138export const SCAN_STATUS : Record < number , string > = {
110139 0 : 'Queued' ,
111140 1 : 'Running' ,
@@ -135,6 +164,7 @@ export class SastApi {
135164 private client : AxiosInstance ;
136165 private token : string = '' ;
137166 private baseUrl : string = '' ;
167+ private httpsAgent : https . Agent | undefined ;
138168
139169 constructor ( ) {
140170 this . client = axios . create ( { timeout : 600000 } ) ;
@@ -145,10 +175,22 @@ export class SastApi {
145175 const config = vscode . workspace . getConfiguration ( 'o360' ) ;
146176 this . baseUrl = ( config . get < string > ( 'endpoint' ) || 'https://sast.offensive360.com' ) . replace ( / \/ + $ / , '' ) ;
147177 this . token = config . get < string > ( 'accessToken' ) || '' ;
148- this . client . defaults . baseURL = this . baseUrl ;
149- if ( this . token ) {
150- this . client . defaults . headers . common [ 'Authorization' ] = `Bearer ${ this . token } ` ;
178+ const allowSelfSigned = config . get < boolean > ( 'allowSelfSignedCerts' ) || false ;
179+
180+ if ( allowSelfSigned ) {
181+ this . httpsAgent = new https . Agent ( { rejectUnauthorized : false } ) ;
182+ process . env . NODE_TLS_REJECT_UNAUTHORIZED = '0' ;
183+ } else {
184+ this . httpsAgent = undefined ;
185+ delete process . env . NODE_TLS_REJECT_UNAUTHORIZED ;
151186 }
187+
188+ this . client = axios . create ( {
189+ baseURL : this . baseUrl ,
190+ timeout : 600000 ,
191+ httpsAgent : this . httpsAgent ,
192+ headers : this . token ? { 'Authorization' : `Bearer ${ this . token } ` } : { }
193+ } ) ;
152194 }
153195
154196 isAuthenticated ( ) : boolean {
@@ -236,12 +278,21 @@ export class SastApi {
236278 return response . data ;
237279 }
238280
281+ /**
282+ * Deletes a project from the server to avoid leaving scan artifacts in the dashboard.
283+ */
284+ async deleteProject ( projectId : string ) : Promise < void > {
285+ try {
286+ await this . client . delete ( `/app/api/Project/${ projectId } ` ) ;
287+ } catch {
288+ // best-effort cleanup
289+ }
290+ }
291+
239292 async scanFileUpload ( zipPath : string , projectName : string ) : Promise < any > {
240293 const form = new FormData ( ) ;
241294 form . append ( 'FileSource' , fs . createReadStream ( zipPath ) ) ;
242295 form . append ( 'Name' , projectName ) ;
243- // KeepInvisibleAndDeletePostScan disabled until server-side delayed deletion is fixed
244- // form.append('KeepInvisibleAndDeletePostScan', 'True');
245296 form . append ( 'ExternalScanSourceType' , 'VsCodeExtension' ) ;
246297
247298 const response = await this . client . post ( '/app/api/Project/scanProjectFile' , form , {
@@ -251,11 +302,71 @@ export class SastApi {
251302 } ,
252303 maxContentLength : Infinity ,
253304 maxBodyLength : Infinity ,
254- timeout : 600000
305+ timeout : 600000 ,
306+ httpsAgent : this . httpsAgent
307+ } ) ;
308+ return response . data ;
309+ }
310+
311+ /**
312+ * Upload files to /app/api/ExternalScan.
313+ * Returns all results immediately — no polling needed.
314+ * The project is ephemeral and auto-deleted by the server.
315+ */
316+ async externalScan ( zipPath : string , projectName : string , sourceType : string = 'VsCodeExtension' ) : Promise < ExternalScanResponse > {
317+ const form = new FormData ( ) ;
318+ form . append ( 'fileSource' , fs . createReadStream ( zipPath ) ) ;
319+ form . append ( 'Name' , projectName ) ;
320+ form . append ( 'KeepInvisibleAndDeletePostScan' , 'True' ) ;
321+ form . append ( 'ExternalScanSourceType' , sourceType ) ;
322+
323+ const response = await this . client . post ( '/app/api/ExternalScan' , form , {
324+ headers : {
325+ ...form . getHeaders ( ) ,
326+ 'Authorization' : `Bearer ${ this . token } `
327+ } ,
328+ maxContentLength : Infinity ,
329+ maxBodyLength : Infinity ,
330+ timeout : 600000 ,
331+ httpsAgent : this . httpsAgent
255332 } ) ;
256333 return response . data ;
257334 }
258335
336+ /**
337+ * Convert ExternalScan vulnerabilities to LangScanResult format
338+ * for compatibility with diagnostics and tree views.
339+ */
340+ static convertExternalVulns ( vulns : ExternalScanVuln [ ] ) : LangScanResult [ ] {
341+ return vulns . map ( v => {
342+ const parts = ( v . lineNumber || '0,0' ) . split ( ',' ) ;
343+ const lineNo = parseInt ( parts [ 0 ] ) || 0 ;
344+ const columnNo = parseInt ( parts [ 1 ] ) || 0 ;
345+
346+ let snippet = '' ;
347+ if ( v . codeSnippet ) {
348+ try { snippet = Buffer . from ( v . codeSnippet , 'base64' ) . toString ( 'utf8' ) ; } catch { snippet = v . codeSnippet ; }
349+ }
350+
351+ return {
352+ id : v . id ,
353+ fileName : v . fileName ,
354+ filePath : v . filePath ,
355+ lineNo,
356+ columnNo,
357+ codeSnippet : snippet ,
358+ type : v . type ,
359+ riskLevel : v . riskLevel ,
360+ vulnerability : v . vulnerability || v . title ,
361+ references : v . references || '' ,
362+ isTagged : false ,
363+ // Preserve extra fields for richer display
364+ effect : ( v as any ) . effect ,
365+ recommendation : ( v as any ) . recommendation
366+ } as LangScanResult & { effect ?: string ; recommendation ?: string } ;
367+ } ) ;
368+ }
369+
259370 async scanGitRepo ( repoUrl : string , projectName : string , branch ?: string ) : Promise < any > {
260371 const body : any = {
261372 Name : projectName ,
@@ -340,6 +451,23 @@ export class SastApi {
340451 malware : MalwareScanResult [ ] ;
341452 license : LicenseScanResult [ ] ;
342453 } > {
454+ // Retry up to 3 times with 5s delay — some servers need time to populate results after scan completes
455+ for ( let attempt = 0 ; attempt < 3 ; attempt ++ ) {
456+ const [ lang , dep , malware , license ] = await Promise . all ( [
457+ this . getLanguageResults ( projectId ) ,
458+ this . getDependencyResults ( projectId ) ,
459+ this . getMalwareResults ( projectId ) ,
460+ this . getLicenseResults ( projectId ) ,
461+ ] ) ;
462+ const total = lang . length + dep . length + malware . length + license . length ;
463+ if ( total > 0 ) {
464+ return { lang, dep, malware, license } ;
465+ }
466+ if ( attempt < 2 ) {
467+ await new Promise ( resolve => setTimeout ( resolve , 5000 ) ) ;
468+ }
469+ }
470+ // Final attempt
343471 const [ lang , dep , malware , license ] = await Promise . all ( [
344472 this . getLanguageResults ( projectId ) ,
345473 this . getDependencyResults ( projectId ) ,
0 commit comments