From a63d179a0d83bafe03b715398052b61127b473e5 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 4 Jul 2023 22:06:29 -0400 Subject: [PATCH] add email notifications for risks --- secret-engine/.dockerignore | 1 + secret-engine/app.yml | 30 ++--- secret-engine/package-lock.json | 15 +++ secret-engine/package.json | 4 +- secret-engine/src/helper/nodemailer.ts | 45 +++++++ secret-engine/src/index.ts | 59 ++++++++- secret-engine/src/models/membershipOrg.ts | 56 +++++++++ secret-engine/src/models/user.ts | 116 ++++++++++++++++++ secret-engine/src/service/smtp.ts | 77 ++++++++++++ .../templates/secretLeakIncident.handlebars | 19 +++ .../test/fixtures/issues.opened.json | 18 --- secret-engine/test/fixtures/mock-cert.pem | 27 ---- secret-engine/test/index.test.ts | 74 ----------- secret-engine/tsconfig.json | 7 +- 14 files changed, 406 insertions(+), 142 deletions(-) create mode 100644 secret-engine/src/helper/nodemailer.ts create mode 100644 secret-engine/src/models/membershipOrg.ts create mode 100644 secret-engine/src/models/user.ts create mode 100644 secret-engine/src/service/smtp.ts create mode 100644 secret-engine/src/templates/secretLeakIncident.handlebars delete mode 100644 secret-engine/test/fixtures/issues.opened.json delete mode 100644 secret-engine/test/fixtures/mock-cert.pem delete mode 100644 secret-engine/test/index.test.ts diff --git a/secret-engine/.dockerignore b/secret-engine/.dockerignore index c961acb50..88fe8b202 100644 --- a/secret-engine/.dockerignore +++ b/secret-engine/.dockerignore @@ -8,5 +8,6 @@ **/.env **/.editorconfig **/dist +**/lib **/*.pem Dockerfile diff --git a/secret-engine/app.yml b/secret-engine/app.yml index baea2801a..8121db9fa 100644 --- a/secret-engine/app.yml +++ b/secret-engine/app.yml @@ -13,7 +13,7 @@ # The list of events the GitHub App subscribes to. # Uncomment the event names below to enable them. default_events: - # - check_run + - check_run # - check_suite # - commit_comment # - create @@ -23,20 +23,20 @@ default_events: # - fork # - gollum # - issue_comment - - issues + # - issues - push -# - label -# - milestone -# - member -# - membership -# - org_block -# - organization -# - page_build -# - project -# - project_card -# - project_column -# - public -# - pull_request + # - label + # - milestone + # - member + # - membership + # - org_block + # - organization + # - page_build + # - project + # - project_card + # - project_column + # - public + - pull_request # - pull_request_review # - pull_request_review_comment # - release @@ -128,7 +128,7 @@ name: Infisical Radar # The homepage of your GitHub App. url: https://infisical.com/radar # A description of the GitHub App. -description: A description of my awesome app +description: Scan your commits for leaked secrets # Set to true when your GitHub App is available to the public or false when it is only accessible to the owner of the app. # Default: true # public: false diff --git a/secret-engine/package-lock.json b/secret-engine/package-lock.json index 88d013a6d..658dbed54 100644 --- a/secret-engine/package-lock.json +++ b/secret-engine/package-lock.json @@ -9,7 +9,9 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "handlebars": "^4.7.7", "mongoose": "^7.3.1", + "nodemailer": "^6.9.3", "probot": "^12.2.4", "ts-node": "^10.9.1" }, @@ -6301,6 +6303,14 @@ "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", "dev": true }, + "node_modules/nodemailer": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.3.tgz", + "integrity": "sha512-fy9v3NgTzBngrMFkDsKEj0r02U7jm6XfC3b52eoNV+GCrGj+s8pt5OqhiJdWKuw51zCTdiNR/IUD1z33LIIGpg==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "2.0.22", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", @@ -13184,6 +13194,11 @@ "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", "dev": true }, + "nodemailer": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.3.tgz", + "integrity": "sha512-fy9v3NgTzBngrMFkDsKEj0r02U7jm6XfC3b52eoNV+GCrGj+s8pt5OqhiJdWKuw51zCTdiNR/IUD1z33LIIGpg==" + }, "nodemon": { "version": "2.0.22", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", diff --git a/secret-engine/package.json b/secret-engine/package.json index b038c9206..0a8f744e6 100644 --- a/secret-engine/package.json +++ b/secret-engine/package.json @@ -12,13 +12,15 @@ "probot-app" ], "scripts": { - "build": "tsc", + "build": "tsc && cp -R ./src/templates ./lib", "dev": "npm run build && probot run ./lib/index.js", "start": "nodemon -e ts,yml --watch './**/*.ts' --exec npm run dev", "test": "jest" }, "dependencies": { + "handlebars": "^4.7.7", "mongoose": "^7.3.1", + "nodemailer": "^6.9.3", "probot": "^12.2.4", "ts-node": "^10.9.1" }, diff --git a/secret-engine/src/helper/nodemailer.ts b/secret-engine/src/helper/nodemailer.ts new file mode 100644 index 000000000..3aaa2b28a --- /dev/null +++ b/secret-engine/src/helper/nodemailer.ts @@ -0,0 +1,45 @@ +import fs from "fs"; +import path from "path"; +import handlebars from "handlebars"; +import nodemailer from "nodemailer"; + +let smtpTransporter: nodemailer.Transporter; + +/** + * @param {Object} obj + * @param {String} obj.template - email template to use from /templates folder (e.g. testEmail.handlebars) + * @param {String[]} obj.subjectLine - email subject line + * @param {String[]} obj.recipients - email addresses of people to send email to + * @param {Object} obj.substitutions - object containing template substitutions + */ +export const sendMail = async ({ + template, + subjectLine, + recipients, + substitutions, +}: { + template: string; + subjectLine: string; + recipients: string[]; + substitutions: any; +}) => { + const smtpConfigured = process.env.SMTP_HOST == "" || process.env.SMTP_HOST == undefined ? false : true + if (smtpConfigured) { + const html = fs.readFileSync( + path.resolve(__dirname, "../templates/" + template), + "utf8" + ); + const temp = handlebars.compile(html); + const htmlToSend = temp(substitutions); + await smtpTransporter.sendMail({ + from: `"${process.env.SMTP_FROM_NAME}" <${process.env.SMTP_FROM_ADDRESS}>`, + to: recipients.join(", "), + subject: subjectLine, + html: htmlToSend, + }); + } +}; + +export const setTransporter = (transporter: nodemailer.Transporter) => { + smtpTransporter = transporter; +}; \ No newline at end of file diff --git a/secret-engine/src/index.ts b/secret-engine/src/index.ts index 3704fcf8f..067efcfac 100644 --- a/secret-engine/src/index.ts +++ b/secret-engine/src/index.ts @@ -6,6 +6,10 @@ import { join } from "path" import mongoose from "mongoose"; import GitRisks from "./models/gitRisks"; import GitAppOrganizationInstallation from "./models/gitAppOrganizationInstallation"; +import { sendMail, setTransporter } from "./helper/nodemailer"; +import { initSmtp } from "./service/smtp"; +import MembershipOrg, { ADMIN, OWNER } from "./models/membershipOrg"; +import User from "./models/user"; type SecretMatch = { Description: string; @@ -28,10 +32,12 @@ type SecretMatch = { Fingerprint: string; }; -export = (app: Probot) => { +export = async (app: Probot) => { // connect to DB initDatabase() + setTransporter(await initSmtp()); + app.on("installation.created", async (context) => { const { payload } = context; // console.log("payload==>", payload.installation.repository_selection) @@ -52,7 +58,7 @@ export = (app: Probot) => { app.on("push", async (context) => { const { payload } = context; - const { commits, repository, installation, } = payload; + const { commits, repository, installation, pusher } = payload; const [owner, repo] = repository.full_name.split('/'); const installationLinkToOrgExists = await GitAppOrganizationInstallation.findOne({ installationId: installation.id }).lean() @@ -60,8 +66,6 @@ export = (app: Probot) => { return } - console.log("installation link does exist!") - const findingsByFingerprint: { [key: string]: SecretMatch; } = {} for (const commit of commits) { @@ -107,6 +111,53 @@ export = (app: Probot) => { upsert: true }) } + // get emails of admins + const adminsOfWork = await MembershipOrg.find({ + organization: installationLinkToOrgExists.organizationId, + $or: [ + { role: OWNER }, + { role: ADMIN } + ] + }).lean() + + const userEmails = await User.find({ + _id: { + $in: [adminsOfWork.map(orgMembership => orgMembership.user)] + } + }).select("email").lean() + + const adminOrOwnerEmails = userEmails.map(userObject => userObject.email) + + await sendMail({ + template: "secretLeakIncident.handlebars", + subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.full_name}`, + recipients: [pusher.email, ...adminOrOwnerEmails], + substitutions: { + numberOfSecrets: Object.keys(findingsByFingerprint).length, + pusher_email: pusher.email, + pusher_name: pusher.name + } + }); + }); + + app.on(['pull_request.opened', 'pull_request.synchronize'], async (context) => { + const { payload } = context; + const { pull_request } = payload + if (false) { + const check = { + owner: pull_request.head.repo.owner.login, + repo: pull_request.head.repo.name, + name: 'Secret Detection', + head_sha: pull_request.head.sha, + status: 'completed', + conclusion: 'failure', + output: { + title: `X Secrets detected`, + summary: 'We detected potential leaked secret(s) in your pull request.', + }, + }; + return context.octokit.checks.create(check); + } }); }; diff --git a/secret-engine/src/models/membershipOrg.ts b/secret-engine/src/models/membershipOrg.ts new file mode 100644 index 000000000..3af683631 --- /dev/null +++ b/secret-engine/src/models/membershipOrg.ts @@ -0,0 +1,56 @@ +import { Document, Schema, Types, model } from "mongoose"; +// membership roles +export const OWNER = "owner"; +export const ADMIN = "admin"; +export const MEMBER = "member"; + +// membership statuses +export const INVITED = "invited"; + +// -- organization +export const ACCEPTED = "accepted"; + +export interface IMembershipOrg extends Document { + _id: Types.ObjectId; + user: Types.ObjectId; + inviteEmail: string; + organization: Types.ObjectId; + role: "owner" | "admin" | "member"; + status: "invited" | "accepted"; +} + +const membershipOrgSchema = new Schema( + { + user: { + type: Schema.Types.ObjectId, + ref: "User", + }, + inviteEmail: { + type: String, + }, + organization: { + type: Schema.Types.ObjectId, + ref: "Organization", + }, + role: { + type: String, + enum: [OWNER, ADMIN, MEMBER], + required: true, + }, + status: { + type: String, + enum: [INVITED, ACCEPTED], + required: true, + }, + }, + { + timestamps: true, + } +); + +const MembershipOrg = model( + "MembershipOrg", + membershipOrgSchema +); + +export default MembershipOrg; diff --git a/secret-engine/src/models/user.ts b/secret-engine/src/models/user.ts new file mode 100644 index 000000000..0d49b4cd9 --- /dev/null +++ b/secret-engine/src/models/user.ts @@ -0,0 +1,116 @@ +import { Document, Schema, Types, model } from "mongoose"; + +export enum AuthProvider { + GOOGLE = "google", +} + +export interface IUser extends Document { + _id: Types.ObjectId; + authId?: string; + authProvider?: AuthProvider; + email: string; + firstName?: string; + lastName?: string; + encryptionVersion: number; + protectedKey: string; + protectedKeyIV: string; + protectedKeyTag: string; + publicKey?: string; + encryptedPrivateKey?: string; + iv?: string; + tag?: string; + salt?: string; + verifier?: string; + isMfaEnabled: boolean; + mfaMethods: boolean; + devices: { + ip: string; + userAgent: string; + }[]; +} + +const userSchema = new Schema( + { + authId: { + type: String, + }, + authProvider: { + type: String, + enum: AuthProvider, + }, + email: { + type: String, + required: true, + unique: true, + }, + firstName: { + type: String, + }, + lastName: { + type: String, + }, + encryptionVersion: { + type: Number, + select: false, + default: 1, // to resolve backward-compatibility issues + }, + protectedKey: { // introduced as part of encryption version 2 + type: String, + select: false, + }, + protectedKeyIV: { // introduced as part of encryption version 2 + type: String, + select: false, + }, + protectedKeyTag: { // introduced as part of encryption version 2 + type: String, + select: false, + }, + publicKey: { + type: String, + select: false, + }, + encryptedPrivateKey: { + type: String, + select: false, + }, + iv: { // iv of [encryptedPrivateKey] + type: String, + select: false, + }, + tag: { // tag of [encryptedPrivateKey] + type: String, + select: false, + }, + salt: { + type: String, + select: false, + }, + verifier: { + type: String, + select: false, + }, + isMfaEnabled: { + type: Boolean, + default: false, + }, + mfaMethods: [{ + type: String, + }], + devices: { + type: [{ + ip: String, + userAgent: String, + }], + default: [], + select: false, + }, + }, + { + timestamps: true, + } +); + +const User = model("User", userSchema); + +export default User; diff --git a/secret-engine/src/service/smtp.ts b/secret-engine/src/service/smtp.ts new file mode 100644 index 000000000..bd7ae915f --- /dev/null +++ b/secret-engine/src/service/smtp.ts @@ -0,0 +1,77 @@ +import nodemailer from "nodemailer"; +import SMTPConnection from "nodemailer/lib/smtp-connection"; + +export const SMTP_HOST_SENDGRID = "smtp.sendgrid.net"; +export const SMTP_HOST_MAILGUN = "smtp.mailgun.org"; +export const SMTP_HOST_SOCKETLABS = "smtp.socketlabs.com"; +export const SMTP_HOST_ZOHOMAIL = "smtp.zoho.com"; +export const SMTP_HOST_GMAIL = "smtp.gmail.com"; + +export const initSmtp = async () => { + const mailOpts: SMTPConnection.Options = { + host: process.env.SMTP_HOST, + port: process.env.SMTP_PORT, + }; + + if ((process.env.SMTP_USERNAME) && (process.env.SMTP_PASSWORD)) { + mailOpts.auth = { + user: process.env.SMTP_USERNAME, + pass: process.env.SMTP_PASSWORD, + }; + } + + if ((process.env.SMTP_SECURE) ? (process.env.SMTP_SECURE) : false) { + switch (process.env.SMTP_HOST) { + case SMTP_HOST_SENDGRID: + mailOpts.requireTLS = true; + break; + case SMTP_HOST_MAILGUN: + mailOpts.requireTLS = true; + mailOpts.tls = { + ciphers: "TLSv1.2", + } + break; + case SMTP_HOST_SOCKETLABS: + mailOpts.requireTLS = true; + mailOpts.tls = { + ciphers: "TLSv1.2", + } + break; + case SMTP_HOST_ZOHOMAIL: + mailOpts.requireTLS = true; + mailOpts.tls = { + ciphers: "TLSv1.2", + } + break; + case SMTP_HOST_GMAIL: + mailOpts.requireTLS = true; + mailOpts.tls = { + ciphers: "TLSv1.2", + } + break; + default: + if ((process.env.SMTP_HOST).includes("amazonaws.com")) { + mailOpts.tls = { + ciphers: "TLSv1.2", + } + } else { + mailOpts.secure = true; + } + break; + } + } + + const transporter = nodemailer.createTransport(mailOpts); + transporter + .verify() + .then((err) => { + console.log("SMTP - Successfully connected") + }) + .catch(async (err) => { + console.log( + `SMTP - Failed to connect to ${process.env.SMTP_HOST}:${process.env.SMTP_PORT} \n\t${err}` + ); + }); + + return transporter; +}; diff --git a/secret-engine/src/templates/secretLeakIncident.handlebars b/secret-engine/src/templates/secretLeakIncident.handlebars new file mode 100644 index 000000000..10a66fa51 --- /dev/null +++ b/secret-engine/src/templates/secretLeakIncident.handlebars @@ -0,0 +1,19 @@ + + + + + + + Incident alert: secret leaked + + + +

Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push

+

View leaked secrets

+

One or more secret leaks have been detected in a recent commit pushed by {{pusher_name}} ({{pusher_email}}). If + the secrets are test secrets, please mark them as false positives in the Infisical dashboard. + Otherwise, please rotate the secrets immediately.

+ + + \ No newline at end of file diff --git a/secret-engine/test/fixtures/issues.opened.json b/secret-engine/test/fixtures/issues.opened.json deleted file mode 100644 index e570cd41b..000000000 --- a/secret-engine/test/fixtures/issues.opened.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "action": "opened", - "issue": { - "number": 1, - "user": { - "login": "hiimbex" - } - }, - "repository": { - "name": "testing-things", - "owner": { - "login": "hiimbex" - } - }, - "installation": { - "id": 2 - } -} diff --git a/secret-engine/test/fixtures/mock-cert.pem b/secret-engine/test/fixtures/mock-cert.pem deleted file mode 100644 index f2f271c30..000000000 --- a/secret-engine/test/fixtures/mock-cert.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAli7V49NdZe+XYC1pLaHM0te8kiDmZBJ1u2HJHN8GdbROB6NO -VpC3xK7NxQn6xpvZ9ux20NvcDvGle+DOptZztBH+np6h2jZQ1/kD1yG1eQvVH4th -/9oqHuIjmIfO8lIe4Hyd5Fw5xHkGqVETTGR+0c7kdZIlHmkOregUGtMYZRUi4YG+ -q0w+uFemiHpGKXbeCIAvkq7aIkisEzvPWfSyYdA6WJHpxFk7tD7D8VkzABLVRHCq -AuyqPG39BhGZcGLXx5rGK56kDBJkyTR1t3DkHpwX+JKNG5UYNwOG4LcQj1fteeta -TdkYUMjIyWbanlMYyC+dq7B5fe7el99jXQ1gXwIDAQABAoIBADKfiPOpzKLOtzzx -MbHzB0LO+75aHq7+1faayJrVxqyoYWELuB1P3NIMhknzyjdmU3t7S7WtVqkm5Twz -lBUC1q+NHUHEgRQ4GNokExpSP4SU63sdlaQTmv0cBxmkNarS6ZuMBgDy4XoLvaYX -MSUf/uukDLhg0ehFS3BteVFtdJyllhDdTenF1Nb1rAeN4egt8XLsE5NQDr1szFEG -xH5lb+8EDtzgsGpeIddWR64xP0lDIKSZWst/toYKWiwjaY9uZCfAhvYQ1RsO7L/t -sERmpYgh+rAZUh/Lr98EI8BPSPhzFcSHmtqzzejvC5zrZPHcUimz0CGA3YBiLoJX -V1OrxmECgYEAxkd8gpmVP+LEWB3lqpSvJaXcGkbzcDb9m0OPzHUAJDZtiIIf0UmO -nvL68/mzbCHSj+yFjZeG1rsrAVrOzrfDCuXjAv+JkEtEx0DIevU1u60lGnevOeky -r8Be7pmymFB9/gzQAd5ezIlTv/COgoO986a3h1yfhzrrzbqSiivw308CgYEAwecI -aZZwqH3GifR+0+Z1B48cezA5tC8LZt5yObGzUfxKTWy30d7lxe9N59t0KUVt/QL5 -qVkd7mqGzsUMyxUN2U2HVnFTWfUFMhkn/OnCnayhILs8UlCTD2Xxoy1KbQH/9FIr -xf0pbMNJLXeGfyRt/8H+BzSZKBw9opJBWE4gqfECgYBp9FdvvryHuBkt8UQCRJPX -rWsRy6pY47nf11mnazpZH5Cmqspv3zvMapF6AIxFk0leyYiQolFWvAv+HFV5F6+t -Si1mM8GCDwbA5zh6pEBDewHhw+UqMBh63HSeUhmi1RiOwrAA36CO8i+D2Pt+eQHv -ir52IiPJcs4BUNrv5Q1BdwKBgBHgVNw3LGe8QMOTMOYkRwHNZdjNl2RPOgPf2jQL -d/bFBayhq0jD/fcDmvEXQFxVtFAxKAc+2g2S8J67d/R5Gm/AQAvuIrsWZcY6n38n -pfOXaLt1x5fnKcevpFlg4Y2vM4O416RHNLx8PJDehh3Oo/2CSwMrDDuwbtZAGZok -icphAoGBAI74Tisfn+aeCZMrO8KxaWS5r2CD1KVzddEMRKlJvSKTY+dOCtJ+XKj1 -OsZdcDvDC5GtgcywHsYeOWHldgDWY1S8Z/PUo4eK9qBXYBXp3JEZQ1dqzFdz+Txi -rBn2WsFLsxV9j2/ugm0PqWVBcU2bPUCwvaRu3SOms2teaLwGCkhr ------END RSA PRIVATE KEY----- diff --git a/secret-engine/test/index.test.ts b/secret-engine/test/index.test.ts deleted file mode 100644 index d3a6e556e..000000000 --- a/secret-engine/test/index.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -// You can import your modules -// import index from '../src/index' - -import nock from "nock"; -// Requiring our app implementation -import myProbotApp from "../src"; -import { Probot, ProbotOctokit } from "probot"; -// Requiring our fixtures -import payload from "./fixtures/issues.opened.json"; -const issueCreatedBody = { body: "Thanks for opening this issue!" }; -const fs = require("fs"); -const path = require("path"); - -const privateKey = fs.readFileSync( - path.join(__dirname, "fixtures/mock-cert.pem"), - "utf-8" -); - -describe("My Probot app", () => { - let probot: any; - - beforeEach(() => { - nock.disableNetConnect(); - probot = new Probot({ - appId: 123, - privateKey, - // disable request throttling and retries for testing - Octokit: ProbotOctokit.defaults({ - retry: { enabled: false }, - throttle: { enabled: false }, - }), - }); - // Load our app into probot - probot.load(myProbotApp); - }); - - test("creates a comment when an issue is opened", async () => { - const mock = nock("https://api.github.com") - // Test that we correctly return a test token - .post("/app/installations/2/access_tokens") - .reply(200, { - token: "test", - permissions: { - issues: "write", - }, - }) - - // Test that a comment is posted - .post("/repos/hiimbex/testing-things/issues/1/comments", (body: any) => { - expect(body).toMatchObject(issueCreatedBody); - return true; - }) - .reply(200); - - // Receive a webhook event - await probot.receive({ name: "issues", payload }); - - expect(mock.pendingMocks()).toStrictEqual([]); - }); - - afterEach(() => { - nock.cleanAll(); - nock.enableNetConnect(); - }); -}); - -// For more information about testing with Jest see: -// https://facebook.github.io/jest/ - -// For more information about using TypeScript in your tests, Jest recommends: -// https://github.com/kulshekhar/ts-jest - -// For more information about testing with Nock see: -// https://github.com/nock/nock diff --git a/secret-engine/tsconfig.json b/secret-engine/tsconfig.json index bb2122a1c..1c77d2a21 100644 --- a/secret-engine/tsconfig.json +++ b/secret-engine/tsconfig.json @@ -50,7 +50,7 @@ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ /* Source Map Options */ - // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + "sourceRoot": "src", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ @@ -64,7 +64,8 @@ "skipLibCheck": true }, "include": [ - "src/" + "src/**/*", + "src/templates/**/*" ], - "compileOnSave": false + // "compileOnSave": false } \ No newline at end of file