mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
add email notifications for risks
This commit is contained in:
@@ -8,5 +8,6 @@
|
||||
**/.env
|
||||
**/.editorconfig
|
||||
**/dist
|
||||
**/lib
|
||||
**/*.pem
|
||||
Dockerfile
|
||||
|
||||
@@ -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
|
||||
|
||||
15
secret-engine/package-lock.json
generated
15
secret-engine/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
45
secret-engine/src/helper/nodemailer.ts
Normal file
45
secret-engine/src/helper/nodemailer.ts
Normal file
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
56
secret-engine/src/models/membershipOrg.ts
Normal file
56
secret-engine/src/models/membershipOrg.ts
Normal file
@@ -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<IMembershipOrg>(
|
||||
"MembershipOrg",
|
||||
membershipOrgSchema
|
||||
);
|
||||
|
||||
export default MembershipOrg;
|
||||
116
secret-engine/src/models/user.ts
Normal file
116
secret-engine/src/models/user.ts
Normal file
@@ -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<IUser>(
|
||||
{
|
||||
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<IUser>("User", userSchema);
|
||||
|
||||
export default User;
|
||||
77
secret-engine/src/service/smtp.ts
Normal file
77
secret-engine/src/service/smtp.ts
Normal file
@@ -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;
|
||||
};
|
||||
19
secret-engine/src/templates/secretLeakIncident.handlebars
Normal file
19
secret-engine/src/templates/secretLeakIncident.handlebars
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Incident alert: secret leaked</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push</h3>
|
||||
<p><a href="https://app.infisical.com/secret-scanning"><strong>View leaked secrets</strong></a></p>
|
||||
<p>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 <a
|
||||
href="https://app.infisical.com/">Infisical dashboard</a>.
|
||||
Otherwise, please rotate the secrets immediately.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
18
secret-engine/test/fixtures/issues.opened.json
vendored
18
secret-engine/test/fixtures/issues.opened.json
vendored
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"action": "opened",
|
||||
"issue": {
|
||||
"number": 1,
|
||||
"user": {
|
||||
"login": "hiimbex"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"name": "testing-things",
|
||||
"owner": {
|
||||
"login": "hiimbex"
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"id": 2
|
||||
}
|
||||
}
|
||||
27
secret-engine/test/fixtures/mock-cert.pem
vendored
27
secret-engine/test/fixtures/mock-cert.pem
vendored
@@ -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-----
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user