Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 7886348

Browse files
committed
Initial commit
0 parents  commit 7886348

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+14041
-0
lines changed

.editorconfig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[*]
2+
end_of_line = lf
3+
insert_final_newline = true
4+
indent_style = space
5+
indent_size = 2

.eslintrc.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module.exports = {
2+
extends: 'standard-with-typescript',
3+
parserOptions: {
4+
project: './tsconfig.json'
5+
},
6+
rules: {
7+
'@typescript-eslint/explicit-function-return-type': 'off',
8+
'@typescript-eslint/strict-boolean-expressions': 'off',
9+
'@typescript-eslint/no-non-null-assertion': 'off',
10+
'@typescript-eslint/no-dynamic-delete': 'off',
11+
'@typescript-eslint/dot-notation': 'off'
12+
}
13+
}

.gitignore

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
lerna-debug.log*
8+
9+
# Diagnostic reports (https://nodejs.org/api/report.html)
10+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11+
12+
# Runtime data
13+
pids
14+
*.pid
15+
*.seed
16+
*.pid.lock
17+
18+
# Directory for instrumented libs generated by jscoverage/JSCover
19+
lib-cov
20+
21+
# Coverage directory used by tools like istanbul
22+
coverage
23+
24+
# nyc test coverage
25+
.nyc_output
26+
27+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
28+
.grunt
29+
30+
# Bower dependency directory (https://bower.io/)
31+
bower_components
32+
33+
# node-waf configuration
34+
.lock-wscript
35+
36+
# Compiled binary addons (https://nodejs.org/api/addons.html)
37+
build/Release
38+
39+
# Dependency directories
40+
node_modules/
41+
jspm_packages/
42+
43+
# TypeScript v1 declaration files
44+
typings/
45+
46+
# Optional npm cache directory
47+
.npm
48+
49+
# Optional eslint cache
50+
.eslintcache
51+
52+
# Optional REPL history
53+
.node_repl_history
54+
55+
# Output of 'npm pack'
56+
*.tgz
57+
58+
# Yarn Integrity file
59+
.yarn-integrity
60+
61+
# dotenv environment variables file
62+
.env
63+
.env.test
64+
65+
# parcel-bundler cache (https://parceljs.org/)
66+
.cache
67+
68+
# next.js build output
69+
.next
70+
71+
# nuxt.js build output
72+
.nuxt
73+
74+
# vuepress build output
75+
.vuepress/dist
76+
77+
# Serverless directories
78+
.serverless/
79+
80+
# FuseBox cache
81+
.fusebox/
82+
83+
# DynamoDB Local files
84+
.dynamodb/
85+
86+
# TypeScript output
87+
dist
88+
out
89+
90+
# Azure Functions artifacts
91+
bin
92+
obj
93+
appsettings.json
94+
local.settings.json
95+
96+
.idea
97+
.build
98+
.DS_Store

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v12.20.1

DLPTrigger/handlers/get.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Boom from '@hapi/boom'
2+
import * as aws from 'aws-lambda'
3+
4+
import { getItem } from '../models/AdoWorkItemsDlpStatus'
5+
6+
/**
7+
* Handles HTTP get event.
8+
* @param event
9+
* @param context
10+
*/
11+
export default async function handleGetRequest (event: aws.APIGatewayEvent, context: aws.Context) {
12+
const projectId = event.queryStringParameters?.project_id
13+
if (!projectId) {
14+
throw Boom.badRequest('project_id is required')
15+
}
16+
const resourceId = event.queryStringParameters?.resource_id
17+
if (!resourceId) {
18+
throw Boom.badRequest('resource_id is required')
19+
}
20+
return await getItem(projectId, resourceId)
21+
}

DLPTrigger/handlers/post.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import * as aws from 'aws-lambda'
2+
import Boom from '@hapi/boom'
3+
import _ from 'lodash'
4+
import { htmlToText } from 'html-to-text'
5+
6+
import { identifyPII } from '../utils/presidio'
7+
import { DlpStatus, getItem, setNoErrors, StatusFields } from '../models/AdoWorkItemsDlpStatus'
8+
9+
import {
10+
FieldMapItem,
11+
PROJECT_ID_FIELD_PATH,
12+
WORKITEM_TYPE_FIELD_PATHS,
13+
RESOURCE_ID_FIELD_PATHS,
14+
TARGET_FIELDS,
15+
WORKITEM_TYPES
16+
} from '../utils/field-path-map'
17+
18+
/**
19+
* Field map record interface.
20+
*/
21+
interface FieldMapRecord {
22+
startIndex: number | null
23+
endIndex: number | null
24+
fieldMapItem: FieldMapItem
25+
found: boolean
26+
isString: boolean
27+
rawValue: string | null
28+
processedValue: string | null
29+
}
30+
31+
/**
32+
* Handles HTTP POST request.
33+
* @param event AWS lambda event
34+
* @param _context AWS lambda context
35+
*/
36+
export default async function handlePostRequest (event: aws.APIGatewayEvent, _context: aws.Context) {
37+
if (_.isNull(event.body)) {
38+
throw Boom.badRequest('Request does not have a body.')
39+
}
40+
const body = JSON.parse(event.body)
41+
let workItemType: WORKITEM_TYPES | undefined
42+
for (const workItemTypeFieldPath of WORKITEM_TYPE_FIELD_PATHS) {
43+
workItemType = _.get(body, workItemTypeFieldPath)
44+
if (workItemType) {
45+
break
46+
}
47+
}
48+
if (!workItemType) {
49+
throw Boom.badRequest(`Did not find expected property at path: ${WORKITEM_TYPE_FIELD_PATHS.map(i => i.join('.')).join(', ')}`)
50+
}
51+
const projectId = _.get(body, PROJECT_ID_FIELD_PATH)
52+
if (!projectId) {
53+
throw Boom.badRequest(`Did not find expected property at path: ${PROJECT_ID_FIELD_PATH.join(', ')}`)
54+
}
55+
let resourceId: string | undefined
56+
for (const resourceIdFieldPath of RESOURCE_ID_FIELD_PATHS) {
57+
resourceId = _.get(body, resourceIdFieldPath)
58+
if (resourceId) {
59+
break
60+
}
61+
}
62+
if (!resourceId) {
63+
throw Boom.badRequest(`Did not find expected property at path: ${RESOURCE_ID_FIELD_PATHS.map(i => i.join('.')).join(', ')}`)
64+
}
65+
const fieldMap = TARGET_FIELDS[workItemType]
66+
if (!fieldMap) {
67+
throw Boom.badRequest(`Unexpected resource of type ${workItemType}. Accepted values: ${Object.keys(TARGET_FIELDS).join(', ')}.`)
68+
}
69+
const records: FieldMapRecord[] = []
70+
let recordString: string = ''
71+
for (const fieldItem of fieldMap) {
72+
let fieldValue: string | null = null
73+
for (const fieldPath of fieldItem.fieldPaths) {
74+
fieldValue = _.get(body, fieldPath)
75+
if (fieldValue) {
76+
break
77+
}
78+
}
79+
if (!fieldValue || !_.isString(fieldValue)) {
80+
records.push({
81+
startIndex: null,
82+
endIndex: null,
83+
fieldMapItem: fieldItem,
84+
found: !!fieldValue,
85+
isString: _.isString(fieldValue),
86+
rawValue: null,
87+
processedValue: null
88+
})
89+
continue
90+
}
91+
const plainTextFieldValue: string = htmlToText(fieldValue)
92+
records.push({
93+
startIndex: recordString.length,
94+
endIndex: recordString.length + plainTextFieldValue.length + 1,
95+
fieldMapItem: fieldItem,
96+
found: true,
97+
isString: true,
98+
rawValue: fieldValue,
99+
processedValue: plainTextFieldValue
100+
})
101+
recordString = `${recordString}${plainTextFieldValue}\n`
102+
}
103+
const piiDetailList = await identifyPII(recordString)
104+
const adoWorkItemsDlpStatus = await getItem(projectId, resourceId)!
105+
if (!adoWorkItemsDlpStatus) {
106+
throw Boom.internal('Failed to get work item dlp status')
107+
}
108+
await setNoErrors(adoWorkItemsDlpStatus)
109+
if (_.isNil(piiDetailList)) {
110+
for (const statusField of Object.values(StatusFields)) {
111+
adoWorkItemsDlpStatus[statusField].status = DlpStatus.NO_ISSUES
112+
adoWorkItemsDlpStatus[statusField].issues = []
113+
}
114+
return { message: 'OK' }
115+
}
116+
for (const piiDetail of piiDetailList) {
117+
const startIdx = piiDetail.start
118+
for (const recordItem of records) {
119+
if (!recordItem.isString || !recordItem.found) {
120+
continue
121+
}
122+
if (recordItem.startIndex! <= startIdx && startIdx < recordItem.endIndex!) {
123+
const piiMatch = recordString.substr(piiDetail.start, piiDetail.end - piiDetail.start)
124+
adoWorkItemsDlpStatus[recordItem.fieldMapItem.dbField].status = DlpStatus.ISSUES_FOUND
125+
adoWorkItemsDlpStatus[recordItem.fieldMapItem.dbField].issues.push({
126+
text: piiMatch,
127+
score: piiDetail.score
128+
})
129+
break
130+
}
131+
}
132+
}
133+
for (const statusField of Object.values(StatusFields)) {
134+
if (adoWorkItemsDlpStatus[statusField].status !== DlpStatus.UNSCANNED) {
135+
continue
136+
}
137+
adoWorkItemsDlpStatus[statusField].status = DlpStatus.NO_ISSUES
138+
adoWorkItemsDlpStatus[statusField].issues = []
139+
}
140+
let dlpStatus = DlpStatus.NO_ISSUES
141+
for (const statusField of Object.values(StatusFields)) {
142+
if (adoWorkItemsDlpStatus[statusField].status === DlpStatus.ISSUES_FOUND) {
143+
dlpStatus = DlpStatus.ISSUES_FOUND
144+
break
145+
}
146+
}
147+
adoWorkItemsDlpStatus.dlpStatus = dlpStatus
148+
await adoWorkItemsDlpStatus.save()
149+
return { message: 'OK' }
150+
}

DLPTrigger/lambda.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import handleGetRequest from './handlers/get'
2+
import * as aws from 'aws-lambda'
3+
import handlePostRequest from './handlers/post'
4+
5+
/**
6+
* HTTP headers to be always returned to the client.
7+
*/
8+
const commonHeaders = {
9+
'Access-Control-Allow-Credentials': 'true',
10+
'Access-Control-Allow-Origin': '*',
11+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, HANDSHAKE',
12+
'Access-Control-Allow-Headers': '*',
13+
'Access-Control-Max-Age': '86400',
14+
// eslint-disable-next-line quote-props
15+
'Vary': 'Accept-Encoding, Origin',
16+
'Content-Type': 'application/json'
17+
}
18+
19+
/**
20+
* Entry point for the AWS lambda event handler.
21+
* @param event AWS lambda event
22+
* @param context AWS lambda context
23+
*/
24+
export async function handle (event: aws.APIGatewayEvent, context: aws.Context) {
25+
const headers = commonHeaders
26+
let statusCode = 200
27+
let body: any = null
28+
29+
try {
30+
switch (event.httpMethod) {
31+
case 'GET': {
32+
body = await handleGetRequest(event, context)
33+
break
34+
}
35+
case 'POST': {
36+
body = await handlePostRequest(event, context)
37+
break
38+
}
39+
case 'OPTIONS':
40+
// Nothing to do for OPTIONS method.
41+
break
42+
default:
43+
statusCode = 405
44+
body = { message: 'Unsupported Method.' }
45+
break
46+
}
47+
} catch (err) {
48+
if (err.isBoom) {
49+
statusCode = err.output.statusCode
50+
body = {
51+
success: false,
52+
message: err.message,
53+
...(err.data ? { data: err.data } : {})
54+
}
55+
} else {
56+
console.error(`Error occurred: ${err.message as string}`)
57+
console.error(err.stack)
58+
statusCode = 500
59+
body = { message: 'Internal Server Error' }
60+
}
61+
}
62+
63+
return { statusCode, headers, body: body ? JSON.stringify(body) : body }
64+
}

0 commit comments

Comments
 (0)