Skip to content

Commit 4ecaa0a

Browse files
authored
Added support for file scanning
File scanning support
2 parents 2468fe9 + 205f70e commit 4ecaa0a

File tree

7 files changed

+255
-34
lines changed

7 files changed

+255
-34
lines changed

src/base.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
export class Base {
2+
protected readonly API_HOST = 'https://api.nightfall.ai'
3+
protected readonly API_KEY: string = ''
4+
protected readonly AXIOS_HEADERS: { [key: string]: string | number } = {}
5+
6+
constructor(apiKey?: string) {
7+
if (!apiKey && !process.env.hasOwnProperty('NIGHTFALL_API_KEY')) {
8+
throw new Error('Please provide an API Key or configure your key as an environment variable.')
9+
}
10+
11+
this.API_KEY = apiKey || process.env.NIGHTFALL_API_KEY as string
12+
13+
// Set Axios request headers since we will reuse this quite a lot
14+
this.AXIOS_HEADERS = {
15+
'Authorization': `Bearer ${this.API_KEY}`,
16+
'Content-Type': 'application/json',
17+
"User-Agent": "nightfall-nodejs-sdk/1.0.0"
18+
}
19+
}
20+
21+
/**
22+
* A helpder function to determine whether the error is a generic JavaScript error or a
23+
* Nightfall API error.
24+
*
25+
* @param error The error object
26+
* @returns A boolean that indicates if the error is a Nightfall error
27+
*/
28+
protected isNightfallError(error: any): boolean {
29+
if (
30+
error.hasOwnProperty('isAxiosError')
31+
&& error.isAxiosError
32+
&& error.response.data.hasOwnProperty('code')
33+
&& error.response.data.hasOwnProperty('message')
34+
) {
35+
return true
36+
}
37+
38+
return false
39+
}
40+
}

src/filesScanner.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import axios, { AxiosResponse } from "axios"
2+
import fs from 'fs'
3+
import { Base } from './base'
4+
import { ScanFile } from './types'
5+
6+
export class FileScanner extends Base {
7+
filePath: string
8+
fileId: string = ''
9+
chunkSize: number = 0
10+
11+
/**
12+
* Create an instance of the FileScanner helper.
13+
*
14+
* @param apiKey Your Nightfall API key
15+
* @param filePath The path of the file that needs to be scanned
16+
*/
17+
constructor(apiKey: string, filePath: string) {
18+
super(apiKey)
19+
this.filePath = filePath
20+
}
21+
22+
/**
23+
* Reads the file from disk and creates a new file upload session. If this operation
24+
* returns successfully, the ID returned as part of the response object shall be used
25+
* to refer to the file in all subsequent upload and scanning operations.
26+
*
27+
* @returns A promise representing the Nightfall response
28+
*/
29+
async initialize(): Promise<AxiosResponse<ScanFile.InitializeResponse>> {
30+
try {
31+
// Get the file size and mime type
32+
const stats = fs.statSync(this.filePath)
33+
34+
// Call API
35+
const response = await axios.post<ScanFile.InitializeResponse>(
36+
`${this.API_HOST}/v3/upload`,
37+
{
38+
fileSizeBytes: stats.size,
39+
},
40+
{ headers: this.AXIOS_HEADERS },
41+
)
42+
43+
// Save file ID and chunk size
44+
this.fileId = response.data.id
45+
this.chunkSize = response.data.chunkSize
46+
47+
return Promise.resolve(response)
48+
} catch (error) {
49+
return Promise.reject(error)
50+
}
51+
}
52+
53+
/**
54+
* Read the file, break it up into chunks and upload each chunk.
55+
*/
56+
async uploadChunks() {
57+
try {
58+
// Read the file in chunks
59+
const stream = fs.createReadStream(this.filePath, {
60+
highWaterMark: this.chunkSize,
61+
encoding: 'utf8'
62+
})
63+
64+
// Upload chunks
65+
let uploadOffset = 0
66+
67+
for await (const chunk of stream) {
68+
await axios.patch(`${this.API_HOST}/v3/upload/${this.fileId}`, chunk, {
69+
headers: {
70+
...this.AXIOS_HEADERS,
71+
'Content-Type': 'application/octet-stream',
72+
'X-Upload-Offset': uploadOffset
73+
}
74+
})
75+
76+
uploadOffset += this.chunkSize
77+
}
78+
79+
return Promise.resolve()
80+
} catch (error) {
81+
return Promise.reject(error)
82+
}
83+
}
84+
85+
/**
86+
* Marks an upload as 'finished' once all the chunks are uploaded.
87+
* This step is necessary to begin scanning.
88+
*
89+
* @returns A promise representing the API response
90+
*/
91+
async finish(): Promise<AxiosResponse<ScanFile.FinishUploadResponse>> {
92+
try {
93+
const response = await axios.post<ScanFile.FinishUploadResponse>(
94+
`${this.API_HOST}/v3/upload/${this.fileId}/finish`,
95+
{},
96+
{ headers: this.AXIOS_HEADERS }
97+
)
98+
99+
return Promise.resolve(response)
100+
} catch (error) {
101+
return Promise.reject(error)
102+
}
103+
}
104+
105+
/**
106+
* Triggers a scan of the uploaded file.
107+
*
108+
* @param policy An object containing the scan policy
109+
* @param requestMetadata The optional request metadata
110+
* @returns A promise representing the API response
111+
*/
112+
async scan(policy: ScanFile.ScanPolicy, requestMetadata?: string): Promise<AxiosResponse<ScanFile.ScanResponse>> {
113+
try {
114+
// Only send the requestMetadata if provided
115+
const data: ScanFile.ScanRequest = { policy }
116+
if (requestMetadata) {
117+
data.requestMetadata = requestMetadata
118+
}
119+
120+
const response = await axios.post<ScanFile.ScanResponse>(
121+
`${this.API_HOST}/v3/upload/${this.fileId}/scan`,
122+
data,
123+
{ headers: this.AXIOS_HEADERS },
124+
)
125+
126+
return Promise.resolve(response)
127+
} catch (error) {
128+
return Promise.reject(error)
129+
}
130+
}
131+
}

src/nightfall.ts

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,20 @@
11
import axios, { AxiosError } from 'axios'
2-
import { NightfallResponse, NightfallError, ScanText } from './types'
3-
4-
export class Nightfall {
5-
private readonly API_HOST = 'https://api.nightfall.ai'
6-
private readonly API_KEY: string = ''
7-
private readonly AXIOS_HEADERS: { [key: string]: string } = {}
2+
import { Base } from './base'
3+
import { FileScanner } from './filesScanner'
4+
import { NightfallResponse, NightfallError, ScanText, ScanFile } from './types'
85

6+
export class Nightfall extends Base {
97
/**
10-
* Create an instance of the Nightfall client.
8+
* Create an instance of the Nightfall client. Although you can supply
9+
* your API key manually when you initiate the client, we recommend that
10+
* you configure your API key as an environment variable named
11+
* NIGHTFALL_API_KEY. The client automatically reads `process.env.NIGHTFALL_API_KEY`
12+
* when you initiate the client like so: `const client = new Nightfall()`.
1113
*
1214
* @param apiKey Your Nightfall API key
1315
*/
14-
constructor(apiKey: string) {
15-
this.API_KEY = apiKey
16-
17-
// Set Axios request headers since we will reuse this quite a lot
18-
this.AXIOS_HEADERS = {
19-
'Authorization': `Bearer ${this.API_KEY}`,
20-
'Content-Type': 'application/json',
21-
"User-Agent": "nightfall-nodejs-sdk/1.0.0"
22-
}
16+
constructor(apiKey?: string) {
17+
super(apiKey)
2318
}
2419

2520
/**
@@ -30,7 +25,7 @@ export class Nightfall {
3025
*
3126
* @param payload An array of strings that you would like to scan
3227
* @param config The configuration to use to scan the payload
33-
* @returns A promise object representing the Nightfall response
28+
* @returns A promise that contains the API response
3429
*/
3530
async scanText(payload: string[], config: ScanText.RequestConfig): Promise<NightfallResponse<ScanText.Response>> {
3631
try {
@@ -54,17 +49,38 @@ export class Nightfall {
5449
}
5550

5651
/**
57-
* A helpder function to determine whether the error is a generic JavaScript error or a
58-
* Nightfall API error.
52+
* A utility method that wraps the four steps related to uploading and scanning files.
53+
* As the underlying file might be arbitrarily large, this scan is conducted
54+
* asynchronously. Results from the scan are delivered to the webhook URL provided in
55+
* the request.
56+
*
57+
* @see https://docs.nightfall.ai/docs/scanning-files
5958
*
60-
* @param error The error object
61-
* @returns A boolean that indicates if the error is a Nightfall error
59+
* @param filePath The path of the file that you wish to scan
60+
* @param policy An object containing the scan policy
61+
* @param requestMetadata Optional - A string containing arbitrary metadata. You may opt to use
62+
* this to help identify your input file upon receiving a webhook response.
63+
* Maximum length 10 KB.
64+
* @returns A promise that contains the API response
6265
*/
63-
private isNightfallError(error: any): boolean {
64-
if (error.hasOwnProperty('isAxiosError') && error.isAxiosError && error.response.data.hasOwnProperty('code')) {
65-
return true
66-
}
66+
async scanFile(filePath: string, policy: ScanFile.ScanPolicy, requestMetadata?: string): Promise<NightfallResponse<ScanFile.ScanResponse>> {
67+
try {
68+
const fileScanner = new FileScanner(this.API_KEY, filePath)
69+
await fileScanner.initialize()
70+
await fileScanner.uploadChunks()
71+
await fileScanner.finish()
72+
const response = await fileScanner.scan(policy, requestMetadata)
6773

68-
return false
74+
return Promise.resolve(new NightfallResponse<ScanFile.ScanResponse>(response.data))
75+
} catch (error) {
76+
if (this.isNightfallError(error)) {
77+
const axiosError = error as AxiosError<NightfallError>
78+
const errorResponse = new NightfallResponse<ScanFile.ScanResponse>()
79+
errorResponse.setError(axiosError.response?.data as NightfallError)
80+
return Promise.resolve(errorResponse)
81+
}
82+
83+
return Promise.reject(error)
84+
}
6985
}
7086
}

src/types/detectors.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,10 @@ export namespace Detector {
7575
exclusionRules?: ExclusionRule[]
7676
redactionConfig?: RedactionConfig
7777
}
78+
79+
export interface Rule {
80+
detectors: Properties[]
81+
name: string
82+
logicalOp: 'ANY' | 'ALL'
83+
}
7884
}

src/types/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './detectors'
22
export * from './global'
3-
export * from './scanText'
3+
export * from './scanFile'
4+
export * from './scanText'

src/types/scanFile.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Detector } from ".";
2+
3+
export namespace ScanFile {
4+
export interface ScanRequest {
5+
policy: ScanPolicy
6+
requestMetadata?: string
7+
}
8+
9+
export interface ScanPolicy {
10+
detectionRuleUUIDs?: string[]
11+
detectionRules?: Detector.Rule[]
12+
webhookURL: string
13+
}
14+
15+
export interface InitializeResponse {
16+
id: string
17+
fileSizeBytes: number
18+
chunkSize: number
19+
mimeType: string
20+
}
21+
22+
export interface FinishUploadResponse {
23+
id: string,
24+
fileSizeBytes: number
25+
chunkSize: number
26+
mimeType: string
27+
}
28+
29+
export interface ScanResponse {
30+
id: string
31+
message: string
32+
}
33+
}

src/types/scanText.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,10 @@ import { Detector } from ".";
33
export namespace ScanText {
44
export interface RequestConfig {
55
detectionRuleUUIDs?: string[]
6-
detectionRules?: DetectionRule[]
6+
detectionRules?: Detector.Rule[]
77
contextBytes?: number
88
}
99

10-
export interface DetectionRule {
11-
detectors: Detector.Properties[]
12-
name: string
13-
logicalOp: 'ANY' | 'ALL'
14-
}
15-
1610
export interface Response {
1711
findings: Finding[][]
1812
redactedPayload: string[]

0 commit comments

Comments
 (0)