Skip to content

Commit 3d86faf

Browse files
authored
Merge pull request #99 from sethRait/serait/checkCallerIsOnboarded
Check caller is onboarded to container mapping before trying to run
2 parents bfad28a + b3a104d commit 3d86faf

File tree

4 files changed

+172
-26
lines changed

4 files changed

+172
-26
lines changed

lib/container-mapping.js

+78-11
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ const core = __importStar(require("@actions/core"));
3838
const exec = __importStar(require("@actions/exec"));
3939
const os = __importStar(require("os"));
4040
const sendReportRetryCount = 1;
41+
const GetScanContextURL = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/auth-push/GetScanContext?context=authOnly";
42+
const ContainerMappingURL = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/container-mappings";
4143
class ContainerMapping {
4244
constructor() {
4345
this.succeedOnError = true;
@@ -90,6 +92,20 @@ class ContainerMapping {
9092
dockerEvents: [],
9193
dockerImages: []
9294
};
95+
let bearerToken = yield core.getIDToken()
96+
.then((token) => { return token; })
97+
.catch((error) => {
98+
throw new Error("Unable to get token: " + error);
99+
});
100+
if (!bearerToken) {
101+
throw new Error("Empty OIDC token received");
102+
}
103+
var callerIsOnboarded = yield this.checkCallerIsCustomer(bearerToken, sendReportRetryCount);
104+
if (!callerIsOnboarded) {
105+
core.info("Client is not onboarded to Defender for DevOps. Skipping container mapping workload.");
106+
return;
107+
}
108+
core.info("Client is onboarded for container mapping.");
93109
let dockerVersionOutput = yield exec.getExecOutput('docker --version');
94110
if (dockerVersionOutput.exitCode != 0) {
95111
core.info(`Unable to get docker version: ${dockerVersionOutput}`);
@@ -106,14 +122,6 @@ class ContainerMapping {
106122
throw new Error("Unable to get docker images: " + error);
107123
});
108124
core.debug("Finished data collection, starting API calls.");
109-
let bearerToken = yield core.getIDToken()
110-
.then((token) => { return token; })
111-
.catch((error) => {
112-
throw new Error("Unable to get token: " + error);
113-
});
114-
if (!bearerToken) {
115-
throw new Error("Empty OIDC token received");
116-
}
117125
var reportSent = yield this.sendReport(JSON.stringify(reportData), bearerToken, sendReportRetryCount);
118126
if (!reportSent) {
119127
throw new Error("Unable to send report to backend service");
@@ -160,7 +168,6 @@ class ContainerMapping {
160168
return __awaiter(this, void 0, void 0, function* () {
161169
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
162170
let apiTime = new Date().getMilliseconds();
163-
let url = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/container-mappings";
164171
let options = {
165172
method: 'POST',
166173
timeout: 2500,
@@ -170,8 +177,8 @@ class ContainerMapping {
170177
'Content-Length': data.length
171178
}
172179
};
173-
core.debug(`${options['method'].toUpperCase()} ${url}`);
174-
const req = https.request(url, options, (res) => {
180+
core.debug(`${options['method'].toUpperCase()} ${ContainerMappingURL}`);
181+
const req = https.request(ContainerMappingURL, options, (res) => {
175182
let resData = '';
176183
res.on('data', (chunk) => {
177184
resData += chunk.toString();
@@ -197,5 +204,65 @@ class ContainerMapping {
197204
}));
198205
});
199206
}
207+
checkCallerIsCustomer(bearerToken, retryCount = 0) {
208+
return __awaiter(this, void 0, void 0, function* () {
209+
return yield this._checkCallerIsCustomer(bearerToken)
210+
.then((statusCode) => __awaiter(this, void 0, void 0, function* () {
211+
if (statusCode == 200) {
212+
return true;
213+
}
214+
else if (statusCode == 403) {
215+
return false;
216+
}
217+
else {
218+
core.debug(`Unexpected status code: ${statusCode}`);
219+
return yield this.retryCall(bearerToken, retryCount);
220+
}
221+
}))
222+
.catch((error) => __awaiter(this, void 0, void 0, function* () {
223+
core.info(`Unexpected error: ${error}.`);
224+
return yield this.retryCall(bearerToken, retryCount);
225+
}));
226+
});
227+
}
228+
retryCall(bearerToken, retryCount) {
229+
return __awaiter(this, void 0, void 0, function* () {
230+
if (retryCount == 0) {
231+
core.info(`All retries failed.`);
232+
return false;
233+
}
234+
else {
235+
core.info(`Retrying checkCallerIsCustomer.\nRetry count: ${retryCount}`);
236+
retryCount--;
237+
return yield this.checkCallerIsCustomer(bearerToken, retryCount);
238+
}
239+
});
240+
}
241+
_checkCallerIsCustomer(bearerToken) {
242+
return __awaiter(this, void 0, void 0, function* () {
243+
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
244+
let options = {
245+
method: 'GET',
246+
timeout: 2500,
247+
headers: {
248+
'Content-Type': 'application/json',
249+
'Authorization': 'Bearer ' + bearerToken,
250+
}
251+
};
252+
core.debug(`${options['method'].toUpperCase()} ${GetScanContextURL}`);
253+
const req = https.request(GetScanContextURL, options, (res) => {
254+
res.on('end', () => {
255+
resolve(res.statusCode);
256+
});
257+
res.on('data', function (d) {
258+
});
259+
});
260+
req.on('error', (error) => {
261+
reject(new Error(`Error calling url: ${error}`));
262+
});
263+
req.end();
264+
}));
265+
});
266+
}
200267
}
201268
exports.ContainerMapping = ContainerMapping;

node_modules/.package-lock.json

+2-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

+4-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/container-mapping.ts

+88-13
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import * as exec from '@actions/exec';
66
import * as os from 'os';
77

88
const sendReportRetryCount: number = 1;
9+
const GetScanContextURL: string = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/auth-push/GetScanContext?context=authOnly";
10+
const ContainerMappingURL: string = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/container-mappings";
911

1012
/**
1113
* Represents the tasks for container mapping that are used to fetch Docker images pushed in a job run.
@@ -85,6 +87,24 @@ export class ContainerMapping implements IMicrosoftSecurityDevOps {
8587
dockerEvents: [],
8688
dockerImages: []
8789
};
90+
91+
let bearerToken: string | void = await core.getIDToken()
92+
.then((token) => { return token; })
93+
.catch((error) => {
94+
throw new Error("Unable to get token: " + error);
95+
});
96+
97+
if (!bearerToken) {
98+
throw new Error("Empty OIDC token received");
99+
}
100+
101+
// Don't run the container mapping workload if this caller isn't an active customer.
102+
var callerIsOnboarded: boolean = await this.checkCallerIsCustomer(bearerToken, sendReportRetryCount);
103+
if (!callerIsOnboarded) {
104+
core.info("Client is not onboarded to Defender for DevOps. Skipping container mapping workload.")
105+
return;
106+
}
107+
core.info("Client is onboarded for container mapping.");
88108

89109
// Initialize the commands
90110
let dockerVersionOutput = await exec.getExecOutput('docker --version');
@@ -107,16 +127,6 @@ export class ContainerMapping implements IMicrosoftSecurityDevOps {
107127

108128
core.debug("Finished data collection, starting API calls.");
109129

110-
let bearerToken: string | void = await core.getIDToken()
111-
.then((token) => { return token; })
112-
.catch((error) => {
113-
throw new Error("Unable to get token: " + error);
114-
});
115-
116-
if (!bearerToken) {
117-
throw new Error("Empty OIDC token received");
118-
}
119-
120130
var reportSent: boolean = await this.sendReport(JSON.stringify(reportData), bearerToken, sendReportRetryCount);
121131
if (!reportSent) {
122132
throw new Error("Unable to send report to backend service");
@@ -148,6 +158,7 @@ export class ContainerMapping implements IMicrosoftSecurityDevOps {
148158
* Sends a report to Defender for DevOps and retries on the specified count
149159
* @param data the data to send
150160
* @param retryCount the number of time to retry
161+
* @param bearerToken the GitHub-generated OIDC token
151162
* @returns a boolean Promise to indicate if the report was sent successfully or not
152163
*/
153164
private async sendReport(data: string, bearerToken: string, retryCount: number = 0): Promise<boolean> {
@@ -175,7 +186,6 @@ export class ContainerMapping implements IMicrosoftSecurityDevOps {
175186
private async _sendReport(data: string, bearerToken: string): Promise<void> {
176187
return new Promise(async (resolve, reject) => {
177188
let apiTime = new Date().getMilliseconds();
178-
let url: string = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/container-mappings";
179189
let options = {
180190
method: 'POST',
181191
timeout: 2500,
@@ -185,9 +195,9 @@ export class ContainerMapping implements IMicrosoftSecurityDevOps {
185195
'Content-Length': data.length
186196
}
187197
};
188-
core.debug(`${options['method'].toUpperCase()} ${url}`);
198+
core.debug(`${options['method'].toUpperCase()} ${ContainerMappingURL}`);
189199

190-
const req = https.request(url, options, (res) => {
200+
const req = https.request(ContainerMappingURL, options, (res) => {
191201
let resData = '';
192202
res.on('data', (chunk) => {
193203
resData += chunk.toString();
@@ -215,4 +225,69 @@ export class ContainerMapping implements IMicrosoftSecurityDevOps {
215225
req.end();
216226
});
217227
}
228+
229+
/**
230+
* Queries Defender for DevOps to determine if the caller is onboarded for container mapping.
231+
* @param retryCount the number of time to retry
232+
* @param bearerToken the GitHub-generated OIDC token
233+
* @returns a boolean Promise to indicate if the report was sent successfully or not
234+
*/
235+
private async checkCallerIsCustomer(bearerToken: string, retryCount: number = 0): Promise<boolean> {
236+
return await this._checkCallerIsCustomer(bearerToken)
237+
.then(async (statusCode) => {
238+
if (statusCode == 200) { // Status 'OK' means the caller is an onboarded customer.
239+
return true;
240+
} else if (statusCode == 403) { // Status 'Forbidden' means caller is not a customer.
241+
return false;
242+
} else {
243+
core.debug(`Unexpected status code: ${statusCode}`);
244+
return await this.retryCall(bearerToken, retryCount);
245+
}
246+
})
247+
.catch(async (error) => {
248+
core.info(`Unexpected error: ${error}.`);
249+
return await this.retryCall(bearerToken, retryCount);
250+
});
251+
}
252+
253+
private async retryCall(bearerToken: string, retryCount: number): Promise<boolean> {
254+
if (retryCount == 0) {
255+
core.info(`All retries failed.`);
256+
return false;
257+
} else {
258+
core.info(`Retrying checkCallerIsCustomer.\nRetry count: ${retryCount}`);
259+
retryCount--;
260+
return await this.checkCallerIsCustomer(bearerToken, retryCount);
261+
}
262+
}
263+
264+
private async _checkCallerIsCustomer(bearerToken: string): Promise<number> {
265+
return new Promise(async (resolve, reject) => {
266+
let options = {
267+
method: 'GET',
268+
timeout: 2500,
269+
headers: {
270+
'Content-Type': 'application/json',
271+
'Authorization': 'Bearer ' + bearerToken,
272+
}
273+
};
274+
core.debug(`${options['method'].toUpperCase()} ${GetScanContextURL}`);
275+
276+
const req = https.request(GetScanContextURL, options, (res) => {
277+
278+
res.on('end', () => {
279+
resolve(res.statusCode);
280+
});
281+
res.on('data', function(d) {
282+
});
283+
});
284+
285+
req.on('error', (error) => {
286+
reject(new Error(`Error calling url: ${error}`));
287+
});
288+
289+
req.end();
290+
});
291+
}
292+
218293
}

0 commit comments

Comments
 (0)