Skip to content

Commit

Permalink
Merge pull request #99 from sethRait/serait/checkCallerIsOnboarded
Browse files Browse the repository at this point in the history
Check caller is onboarded to container mapping before trying to run
  • Loading branch information
sethRait authored Jul 19, 2024
2 parents bfad28a + b3a104d commit 3d86faf
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 26 deletions.
89 changes: 78 additions & 11 deletions lib/container-mapping.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const core = __importStar(require("@actions/core"));
const exec = __importStar(require("@actions/exec"));
const os = __importStar(require("os"));
const sendReportRetryCount = 1;
const GetScanContextURL = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/auth-push/GetScanContext?context=authOnly";
const ContainerMappingURL = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/container-mappings";
class ContainerMapping {
constructor() {
this.succeedOnError = true;
Expand Down Expand Up @@ -90,6 +92,20 @@ class ContainerMapping {
dockerEvents: [],
dockerImages: []
};
let bearerToken = yield core.getIDToken()
.then((token) => { return token; })
.catch((error) => {
throw new Error("Unable to get token: " + error);
});
if (!bearerToken) {
throw new Error("Empty OIDC token received");
}
var callerIsOnboarded = yield this.checkCallerIsCustomer(bearerToken, sendReportRetryCount);
if (!callerIsOnboarded) {
core.info("Client is not onboarded to Defender for DevOps. Skipping container mapping workload.");
return;
}
core.info("Client is onboarded for container mapping.");
let dockerVersionOutput = yield exec.getExecOutput('docker --version');
if (dockerVersionOutput.exitCode != 0) {
core.info(`Unable to get docker version: ${dockerVersionOutput}`);
Expand All @@ -106,14 +122,6 @@ class ContainerMapping {
throw new Error("Unable to get docker images: " + error);
});
core.debug("Finished data collection, starting API calls.");
let bearerToken = yield core.getIDToken()
.then((token) => { return token; })
.catch((error) => {
throw new Error("Unable to get token: " + error);
});
if (!bearerToken) {
throw new Error("Empty OIDC token received");
}
var reportSent = yield this.sendReport(JSON.stringify(reportData), bearerToken, sendReportRetryCount);
if (!reportSent) {
throw new Error("Unable to send report to backend service");
Expand Down Expand Up @@ -160,7 +168,6 @@ class ContainerMapping {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
let apiTime = new Date().getMilliseconds();
let url = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/container-mappings";
let options = {
method: 'POST',
timeout: 2500,
Expand All @@ -170,8 +177,8 @@ class ContainerMapping {
'Content-Length': data.length
}
};
core.debug(`${options['method'].toUpperCase()} ${url}`);
const req = https.request(url, options, (res) => {
core.debug(`${options['method'].toUpperCase()} ${ContainerMappingURL}`);
const req = https.request(ContainerMappingURL, options, (res) => {
let resData = '';
res.on('data', (chunk) => {
resData += chunk.toString();
Expand All @@ -197,5 +204,65 @@ class ContainerMapping {
}));
});
}
checkCallerIsCustomer(bearerToken, retryCount = 0) {
return __awaiter(this, void 0, void 0, function* () {
return yield this._checkCallerIsCustomer(bearerToken)
.then((statusCode) => __awaiter(this, void 0, void 0, function* () {
if (statusCode == 200) {
return true;
}
else if (statusCode == 403) {
return false;
}
else {
core.debug(`Unexpected status code: ${statusCode}`);
return yield this.retryCall(bearerToken, retryCount);
}
}))
.catch((error) => __awaiter(this, void 0, void 0, function* () {
core.info(`Unexpected error: ${error}.`);
return yield this.retryCall(bearerToken, retryCount);
}));
});
}
retryCall(bearerToken, retryCount) {
return __awaiter(this, void 0, void 0, function* () {
if (retryCount == 0) {
core.info(`All retries failed.`);
return false;
}
else {
core.info(`Retrying checkCallerIsCustomer.\nRetry count: ${retryCount}`);
retryCount--;
return yield this.checkCallerIsCustomer(bearerToken, retryCount);
}
});
}
_checkCallerIsCustomer(bearerToken) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
let options = {
method: 'GET',
timeout: 2500,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + bearerToken,
}
};
core.debug(`${options['method'].toUpperCase()} ${GetScanContextURL}`);
const req = https.request(GetScanContextURL, options, (res) => {
res.on('end', () => {
resolve(res.statusCode);
});
res.on('data', function (d) {
});
});
req.on('error', (error) => {
reject(new Error(`Error calling url: ${error}`));
});
req.end();
}));
});
}
}
exports.ContainerMapping = ContainerMapping;
3 changes: 2 additions & 1 deletion node_modules/.package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

101 changes: 88 additions & 13 deletions src/container-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import * as exec from '@actions/exec';
import * as os from 'os';

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

/**
* Represents the tasks for container mapping that are used to fetch Docker images pushed in a job run.
Expand Down Expand Up @@ -85,6 +87,24 @@ export class ContainerMapping implements IMicrosoftSecurityDevOps {
dockerEvents: [],
dockerImages: []
};

let bearerToken: string | void = await core.getIDToken()
.then((token) => { return token; })
.catch((error) => {
throw new Error("Unable to get token: " + error);
});

if (!bearerToken) {
throw new Error("Empty OIDC token received");
}

// Don't run the container mapping workload if this caller isn't an active customer.
var callerIsOnboarded: boolean = await this.checkCallerIsCustomer(bearerToken, sendReportRetryCount);
if (!callerIsOnboarded) {
core.info("Client is not onboarded to Defender for DevOps. Skipping container mapping workload.")
return;
}
core.info("Client is onboarded for container mapping.");

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

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

let bearerToken: string | void = await core.getIDToken()
.then((token) => { return token; })
.catch((error) => {
throw new Error("Unable to get token: " + error);
});

if (!bearerToken) {
throw new Error("Empty OIDC token received");
}

var reportSent: boolean = await this.sendReport(JSON.stringify(reportData), bearerToken, sendReportRetryCount);
if (!reportSent) {
throw new Error("Unable to send report to backend service");
Expand Down Expand Up @@ -148,6 +158,7 @@ export class ContainerMapping implements IMicrosoftSecurityDevOps {
* Sends a report to Defender for DevOps and retries on the specified count
* @param data the data to send
* @param retryCount the number of time to retry
* @param bearerToken the GitHub-generated OIDC token
* @returns a boolean Promise to indicate if the report was sent successfully or not
*/
private async sendReport(data: string, bearerToken: string, retryCount: number = 0): Promise<boolean> {
Expand Down Expand Up @@ -175,7 +186,6 @@ export class ContainerMapping implements IMicrosoftSecurityDevOps {
private async _sendReport(data: string, bearerToken: string): Promise<void> {
return new Promise(async (resolve, reject) => {
let apiTime = new Date().getMilliseconds();
let url: string = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/container-mappings";
let options = {
method: 'POST',
timeout: 2500,
Expand All @@ -185,9 +195,9 @@ export class ContainerMapping implements IMicrosoftSecurityDevOps {
'Content-Length': data.length
}
};
core.debug(`${options['method'].toUpperCase()} ${url}`);
core.debug(`${options['method'].toUpperCase()} ${ContainerMappingURL}`);

const req = https.request(url, options, (res) => {
const req = https.request(ContainerMappingURL, options, (res) => {
let resData = '';
res.on('data', (chunk) => {
resData += chunk.toString();
Expand Down Expand Up @@ -215,4 +225,69 @@ export class ContainerMapping implements IMicrosoftSecurityDevOps {
req.end();
});
}

/**
* Queries Defender for DevOps to determine if the caller is onboarded for container mapping.
* @param retryCount the number of time to retry
* @param bearerToken the GitHub-generated OIDC token
* @returns a boolean Promise to indicate if the report was sent successfully or not
*/
private async checkCallerIsCustomer(bearerToken: string, retryCount: number = 0): Promise<boolean> {
return await this._checkCallerIsCustomer(bearerToken)
.then(async (statusCode) => {
if (statusCode == 200) { // Status 'OK' means the caller is an onboarded customer.
return true;
} else if (statusCode == 403) { // Status 'Forbidden' means caller is not a customer.
return false;
} else {
core.debug(`Unexpected status code: ${statusCode}`);
return await this.retryCall(bearerToken, retryCount);
}
})
.catch(async (error) => {
core.info(`Unexpected error: ${error}.`);
return await this.retryCall(bearerToken, retryCount);
});
}

private async retryCall(bearerToken: string, retryCount: number): Promise<boolean> {
if (retryCount == 0) {
core.info(`All retries failed.`);
return false;
} else {
core.info(`Retrying checkCallerIsCustomer.\nRetry count: ${retryCount}`);
retryCount--;
return await this.checkCallerIsCustomer(bearerToken, retryCount);
}
}

private async _checkCallerIsCustomer(bearerToken: string): Promise<number> {
return new Promise(async (resolve, reject) => {
let options = {
method: 'GET',
timeout: 2500,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + bearerToken,
}
};
core.debug(`${options['method'].toUpperCase()} ${GetScanContextURL}`);

const req = https.request(GetScanContextURL, options, (res) => {

res.on('end', () => {
resolve(res.statusCode);
});
res.on('data', function(d) {
});
});

req.on('error', (error) => {
reject(new Error(`Error calling url: ${error}`));
});

req.end();
});
}

}

0 comments on commit 3d86faf

Please sign in to comment.