├── .secretlintignore ├── .githooks └── pre-commit ├── .mocharc.json ├── docs └── screenshot.png ├── app ├── images │ ├── icon-32.png │ ├── icon-96.png │ └── icon-192.png ├── scripts │ ├── secretlint │ │ ├── rule.allows.ts │ │ ├── lint.ts │ │ └── rule.patterns.ts │ ├── settings.ts │ ├── types.ts │ ├── dev_tools_panel.ts │ ├── dev_tools_panel │ │ ├── base.css │ │ ├── App.css │ │ └── App.tsx │ ├── settings │ │ ├── SettingSchema.ts │ │ ├── SettingSchema.validator.ts │ │ ├── CodeEditor.tsx │ │ └── App.tsx │ ├── contentScript.ts │ ├── background.ts │ └── dev_tools.ts ├── pages │ ├── dev_tools.html │ ├── settings.html │ └── dev_tools_panel.html ├── _locales │ └── en │ │ └── messages.json └── manifest.json ├── demo ├── index.css ├── index.js ├── index.html └── user.json ├── test └── tsconfig.json ├── renovate.json ├── .github └── workflows │ ├── test.yml │ └── codeql-analysis.yml ├── netlify.toml ├── .eslintrc.js ├── tsconfig.json ├── LICENSE ├── webextension-toolbox.config.js ├── package.json ├── .eslintignore ├── .gitignore └── README.md /.secretlintignore: -------------------------------------------------------------------------------- 1 | demo/ 2 | README.md 3 | -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | npx --no-install lint-staged 3 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": [ 3 | "ts-node-test-register" 4 | ] 5 | } -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/secretlint/webextension/HEAD/docs/screenshot.png -------------------------------------------------------------------------------- /app/images/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/secretlint/webextension/HEAD/app/images/icon-32.png -------------------------------------------------------------------------------- /app/images/icon-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/secretlint/webextension/HEAD/app/images/icon-96.png -------------------------------------------------------------------------------- /app/images/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/secretlint/webextension/HEAD/app/images/icon-192.png -------------------------------------------------------------------------------- /app/scripts/secretlint/rule.allows.ts: -------------------------------------------------------------------------------- 1 | export const RULES_DEFAULT_ALLOWS = [ 2 | // local ip 3 | "0.0.0.0", 4 | "1.1.1.1", 5 | "1.2.3.4" 6 | ]; 7 | -------------------------------------------------------------------------------- /demo/index.css: -------------------------------------------------------------------------------- 1 | /* Notice https://hooks.slack.com/services/T11111AA/BAAAA111A/qKisfWavfyBGqKgirRWalryDG */ 2 | html, 3 | body { 4 | width: 100%; 5 | } 6 | 7 | .main { 8 | max-width: 800px; 9 | } 10 | -------------------------------------------------------------------------------- /app/scripts/settings.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { App } from "./settings/App"; 4 | 5 | ReactDOM.render(React.createElement(App), document.querySelector("#main")); 6 | -------------------------------------------------------------------------------- /app/scripts/types.ts: -------------------------------------------------------------------------------- 1 | import { SecretLintCoreResultMessage } from "@secretlint/types"; 2 | 3 | export type SecretLintMessage = SecretLintCoreResultMessage & { 4 | url: string; 5 | sliceContent: string; 6 | }; 7 | -------------------------------------------------------------------------------- /app/scripts/dev_tools_panel.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { App } from "./dev_tools_panel/App"; 4 | 5 | ReactDOM.render(React.createElement(App), document.querySelector("#main")); 6 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "noEmit": true 6 | }, 7 | "include": [ 8 | "../src/**/*", 9 | "./**/*" 10 | ] 11 | } -------------------------------------------------------------------------------- /app/pages/dev_tools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Secretlint DevTools 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/pages/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Secretlint settings 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | fetch("./user.json", { 2 | headers: { 3 | Authorization: "Basic YWxhZGRpbjpvcGVuc2VzYW1l" 4 | } 5 | }) 6 | .then((res) => res.json()) 7 | .then((json) => { 8 | document.getElementById("user-name").textContent = json.name; 9 | }); 10 | -------------------------------------------------------------------------------- /app/pages/dev_tools_panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Secretlint Panel 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>azu/renovate-config:non-major" 4 | ], 5 | "packageRules": [ 6 | { 7 | "groupName": "all dependencies", 8 | "matchPackagePrefixes": ["secretlint", "@secretlint/"], 9 | "major": { 10 | "enabled": true 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /app/scripts/dev_tools_panel/base.css: -------------------------------------------------------------------------------- 1 | details { 2 | padding: 0.6rem 1rem; 3 | } 4 | 5 | summary { 6 | cursor: pointer; 7 | } 8 | 9 | details[open] { 10 | /* Adjust the
padding while open */ 11 | padding-bottom: 0.75rem; 12 | } 13 | 14 | details[open] summary { 15 | /* Adjust the
padding while open */ 16 | margin-bottom: 6px; 17 | } 18 | -------------------------------------------------------------------------------- /app/scripts/settings/SettingSchema.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @title Setting 3 | */ 4 | export type SettingSchema = { 5 | /** 6 | * @title Enable Console Integration 7 | * @default true 8 | */ 9 | enableConsoleIntegration: boolean; 10 | /** 11 | * @title Slice Content before 12 | * @default 32 13 | */ 14 | sliceBefore: number; 15 | /** 16 | * @title Slice Content after 17 | * @default 32 18 | */ 19 | sliceAfter: number; 20 | }; 21 | -------------------------------------------------------------------------------- /app/scripts/dev_tools_panel/App.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | width: 100%; 4 | height: 100%; 5 | margin: 0; 6 | background-color: #ffffff; 7 | } 8 | 9 | .MessageList { 10 | list-style-type: none; 11 | margin: 0; 12 | padding: 0; 13 | } 14 | 15 | .MessageListItem { 16 | padding: 0.5em 1em; 17 | border-bottom: 1px solid #ddd; 18 | display: grid; 19 | grid-template-columns: 1fr 50px; 20 | } 21 | 22 | .MessageNotFound { 23 | padding: 12px 24px; 24 | } 25 | 26 | .Message-ruleId { 27 | font-weight: 600; 28 | } 29 | -------------------------------------------------------------------------------- /app/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "secretlint", 4 | "description": "secretlint founds credentials in yoor request/response." 5 | }, 6 | "appShortName": { 7 | "message": "secretlint", 8 | "description": "secretlint founds credentials in yoor request/response." 9 | }, 10 | "appDescription": { 11 | "message": "Integrate secretlint into DevTools.", 12 | "description": "secretlint founds credentials in yoor request/response." 13 | }, 14 | "browserActionTitle": { 15 | "message": "secretlint", 16 | "description": "secretlint options" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: [push, pull_request] 3 | permissions: 4 | contents: read 5 | jobs: 6 | test: 7 | name: "Test on Node.js ${{ matrix.node-version }}" 8 | runs-on: ubuntu-18.04 9 | strategy: 10 | matrix: 11 | node-version: [12, 14, 16] 12 | steps: 13 | - name: checkout 14 | uses: actions/checkout@v3 15 | - name: setup Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v3 17 | with: 18 | cache: "yarn" 19 | node-version: ${{ matrix.node-version }} 20 | - name: Install 21 | run: yarn install 22 | - name: Test 23 | run: yarn test 24 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | secretlint/webextension demo 6 | 7 | 8 | 9 |
10 |

Demo: secretlint/webextension

11 |

This page exposes credentials.

12 |
13 | 1. Open browser's Developer Tools
14 | 2. ✅ Disable Cache
15 | 3. Reload page and secretlint report found credentials in your request/response.
16 |     
17 |

User Name:

18 |
19 | 22 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | # example netlify.toml 2 | [build] 3 | command = "# no build command" 4 | functions = "functions" 5 | publish = "demo" 6 | 7 | ## Uncomment to use this redirect for Single Page Applications like create-react-app. 8 | ## Not needed for static site generators. 9 | #[[redirects]] 10 | # from = "/*" 11 | # to = "/index.html" 12 | # status = 200 13 | 14 | ## (optional) Settings for Netlify Dev 15 | ## https://github.com/netlify/cli/blob/master/docs/netlify-dev.md#project-detection 16 | #[dev] 17 | # command = "yarn start" # Command to start your dev server 18 | # port = 3000 # Port that the dev server will be listening on 19 | # publish = "dist" # Folder with the static content for _redirect file 20 | 21 | ## more info on configuring this file: https://www.netlify.com/docs/netlify-toml-reference/ 22 | -------------------------------------------------------------------------------- /demo/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tom", 3 | "key": "-----BEGIN RSA PRIVATE KEY-----\nMIICWwIBAAKBgQCYdGaf5uYMsilGHfnx/zxXtihdGFr3hCWwebHGhgEAVn0xlsTd\n1QwoKi+rpI1O6hzyVOuoQtboODsONGRlHbNl6yJ936Yhmr8PiNwpA5qIxZAdmFv2\ntqEllWr0dGPPm3B/2NbjuMpSiJNAcBQa46X++doG5yNMY8NCgTsjBZIBKwIDAQAB\nAoGAN+Pkg5aIm/rsurHeoeMqYhV7srVtE/S0RIA4tkkGMPOELhvRzGmAbXEZzNkk\nnNujBQww4JywYK3MqKZ4b8F1tMG3infs1w8V7INAYY/c8HzfrT3f+MVxijoKV2Fl\nJlUXCclztoZhxAxhCR+WC1Upe1wIrWNwad+JA0Vws/mwrEECQQDxiT/Q0lK+gYaa\n+riFeZmOaqwhlFlYNSK2hCnLz0vbnvnZE5ITQoV+yiy2+BhpMktNFsYNCfb0pdKN\nD87x+jr7AkEAoZWITvqErh1RbMCXd26QXZEfZyrvVZMpYf8BmWFaBXIbrVGme0/Q\nd7amI6B8Vrowyt+qgcUk7rYYaA39jYB7kQJAdaX2sY5gw25v1Dlfe5Q5WYdYBJsv\n0alAGUrS2PVF69nJtRS1SDBUuedcVFsP+N2IlCoNmfhKk+vZXOBgWrkZ1QJAGJlE\nFAntUvhhofW72VG6ppPmPPV7VALARQvmOWxpoPSbJAqPFqyy5tamejv/UdCshuX/\n9huGINUV6BlhJT6PEQJAF/aqQTwZqJdwwJqYEQArSmyOW7UDAlQMmKMofjBbeBvd\nH4PSJT5bvaEhxRj7QCwonoX4ZpV0beTnzloS55Z65g==\n-----END RSA PRIVATE KEY-----\n" 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true 6 | }, 7 | extends: ["plugin:react/recommended"], 8 | parser: "@typescript-eslint/parser", 9 | parserOptions: { 10 | ecmaFeatures: { 11 | jsx: true 12 | }, 13 | ecmaVersion: 12, 14 | sourceType: "module" 15 | }, 16 | plugins: ["react", "react-hooks", "@typescript-eslint"], 17 | rules: { 18 | "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks 19 | "react-hooks/exhaustive-deps": "error" // Checks effect dependencies 20 | }, 21 | settings: { 22 | react: { 23 | pragma: "React", // Pragma to use, default to "React" 24 | fragment: "Fragment", // Fragment to use (may be a property of ), default to "Fragment" 25 | version: "detect" // React version. "detect" automatically picks the version you have installed. 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "module": "CommonJS", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "newLine": "LF", 8 | "outDir": "./lib/", 9 | "target": "ES2019", 10 | "sourceMap": true, 11 | "declaration": false, 12 | "jsx": "react", 13 | "noEmit": false, 14 | "lib": [ 15 | "esnext", 16 | "dom" 17 | ], 18 | /* Strict Type-Checking Options */ 19 | "strict": true, 20 | /* Additional Checks */ 21 | /* Report errors on unused locals. */ 22 | "noUnusedLocals": true, 23 | /* Report errors on unused parameters. */ 24 | "noUnusedParameters": true, 25 | /* Report error when not all code paths in function return a value. */ 26 | "noImplicitReturns": true, 27 | /* Report errors for fallthrough cases in switch statement. */ 28 | "noFallthroughCasesInSwitch": true 29 | }, 30 | "include": [ 31 | "**/*" 32 | ], 33 | "exclude": [ 34 | ".git", 35 | "node_modules" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /app/scripts/contentScript.ts: -------------------------------------------------------------------------------- 1 | import { isInternalEndpoint, onMessage } from "webext-bridge"; 2 | import type { SecretLintMessage } from "./types"; 3 | 4 | onMessage("content-script:log-lint-messages", async (message) => { 5 | const { data, sender } = message; 6 | if (isInternalEndpoint(sender)) { 7 | const messages = data as SecretLintMessage[]; 8 | // FIXME: Firefox does not preserve order, manual sorts 9 | const sortedMessages: SecretLintMessage[] = messages.map((message) => { 10 | return { 11 | ...message, 12 | "1.ruleId": message.ruleId, 13 | "2.message": message.message, 14 | "3.url": message.url, 15 | "4.sliceContent": message.sliceContent 16 | }; 17 | }); 18 | console.group(`Found ${sortedMessages.length} secrets. For more details see Secretlint panel.`); 19 | console.table(sortedMessages, ["1.ruleId", "2.message", "3.url", "4.sliceContent"]); 20 | console.groupEnd(); 21 | } 22 | return {}; 23 | }); 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 azu 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /webextension-toolbox.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const GlobEntriesPlugin = require("webpack-watched-glob-entries-plugin"); 3 | const NodePolyfillPlugin = require("node-polyfill-webpack-plugin"); 4 | 5 | module.exports = { 6 | webpack: (config) => { 7 | // Add typescript loader. supports .ts and .tsx files as entry points 8 | config.resolve.extensions.push(".ts"); 9 | config.resolve.extensions.push(".tsx"); 10 | config.resolve.fallback = { fs: false }; 11 | config.entry = GlobEntriesPlugin.getEntries([ 12 | path.resolve("app", "*.{js,mjs,jsx,ts,tsx}"), 13 | path.resolve("app", "?(scripts)/*.{js,mjs,jsx,ts,tsx}") 14 | ]); 15 | config.module.rules.push({ 16 | test: /\.tsx?$/, 17 | loader: "ts-loader" 18 | }); 19 | config.module.rules.push({ 20 | test: /\.css$/, 21 | use: ["style-loader", "css-loader"] 22 | }); 23 | config.plugins.push(new NodePolyfillPlugin()); 24 | // Important: return the modified config 25 | return config; 26 | }, 27 | copyIgnore: ["**/*.js", "**/*.json", "**/*.ts", "**/*.tsx"] 28 | }; 29 | -------------------------------------------------------------------------------- /app/scripts/secretlint/lint.ts: -------------------------------------------------------------------------------- 1 | import { lintSource } from "@secretlint/core"; 2 | // @ts-ignore 3 | import creator from "@secretlint/secretlint-rule-preset-recommend"; 4 | import pattern from "@secretlint/secretlint-rule-pattern"; 5 | import { RULE_DEFAULT_PATTERNS } from "./rule.patterns"; 6 | 7 | /** 8 | * HeaderName=HeaderValue 9 | */ 10 | export const lintContent = ({ content, url, allows }: { content: string; url: string; allows: string[] }) => { 11 | console.log({ content, url, allows }); 12 | const rules = [ 13 | { 14 | id: "@secretlint/secretlint-rule-preset-recommend", 15 | rule: creator, 16 | options: { 17 | allows 18 | } 19 | }, 20 | { 21 | id: pattern.meta.id, 22 | rule: pattern, 23 | options: { 24 | // based on https://github.com/l4yton/RegHex 25 | patterns: RULE_DEFAULT_PATTERNS, 26 | allows 27 | } 28 | } 29 | ]; 30 | return lintSource({ 31 | source: { 32 | contentType: "text", 33 | content, 34 | filePath: url, 35 | ext: url.split(".").pop() ?? "" 36 | }, 37 | options: { 38 | config: { 39 | rules: rules 40 | } 41 | } 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_appName__", 3 | "short_name": "__MSG_appShortName__", 4 | "description": "__MSG_appDescription__", 5 | "version": "1.3.4", 6 | "manifest_version": 2, 7 | "default_locale": "en", 8 | "icons": { 9 | "32": "images/icon-32.png", 10 | "96": "images/icon-96.png", 11 | "192": "images/icon-192.png" 12 | }, 13 | "background": { 14 | "scripts": [ 15 | "scripts/background.js" 16 | ] 17 | }, 18 | "content_scripts": [ 19 | { 20 | "js": [ 21 | "scripts/contentScript.js" 22 | ], 23 | "matches": [ 24 | "" 25 | ], 26 | "run_at": "document_start" 27 | } 28 | ], 29 | "devtools_page": "pages/dev_tools.html", 30 | "browser_action": { 31 | "default_icon": { 32 | "32": "images/icon-32.png", 33 | "96": "images/icon-96.png", 34 | "192": "images/icon-192.png" 35 | }, 36 | "default_title": "__MSG_browserActionTitle__", 37 | "default_popup": "pages/settings.html" 38 | }, 39 | "options_ui": { 40 | "page": "pages/settings.html" 41 | }, 42 | "permissions": [ 43 | "", 44 | "webNavigation", 45 | "storage" 46 | ], 47 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self';", 48 | "__firefox__applications": { 49 | "gecko": { 50 | "id": "secretlint@secretlint.github.io" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/scripts/settings/SettingSchema.validator.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | // eslint-disable 3 | // This file is generated by create-validator-ts 4 | import Ajv from "ajv"; 5 | import * as apiTypes from "./SettingSchema"; 6 | 7 | export const SCHEMA = { 8 | $schema: "http://json-schema.org/draft-07/schema#", 9 | $ref: "#/definitions/SettingSchema", 10 | definitions: { 11 | SettingSchema: { 12 | type: "object", 13 | properties: { 14 | enableConsoleIntegration: { 15 | type: "boolean", 16 | title: "Enable Console Integration", 17 | default: true 18 | }, 19 | sliceBefore: { 20 | type: "number", 21 | title: "Slice Content before", 22 | default: 32 23 | }, 24 | sliceAfter: { 25 | type: "number", 26 | title: "Slice Content after", 27 | default: 32 28 | } 29 | }, 30 | required: ["enableConsoleIntegration", "sliceBefore", "sliceAfter"], 31 | additionalProperties: false, 32 | title: "Setting" 33 | } 34 | } 35 | }; 36 | const ajv = new Ajv({ removeAdditional: true }).addSchema(SCHEMA, "SCHEMA"); 37 | export function validateSettingSchema(payload: unknown): apiTypes.SettingSchema { 38 | if (!isSettingSchema(payload)) { 39 | const error = new Error("invalid payload: SettingSchema"); 40 | error.name = "ValidationError"; 41 | throw error; 42 | } 43 | return payload; 44 | } 45 | 46 | export function isSettingSchema(payload: unknown): payload is apiTypes.SettingSchema { 47 | /** Schema is defined in {@link SCHEMA.definitions.SettingSchema } **/ 48 | const ajvValidate = ajv.compile({ $ref: "SCHEMA#/definitions/SettingSchema" }); 49 | return ajvValidate(payload); 50 | } 51 | -------------------------------------------------------------------------------- /app/scripts/background.ts: -------------------------------------------------------------------------------- 1 | import { isInternalEndpoint, onMessage } from "webext-bridge"; 2 | import { browser } from "webextension-polyfill-ts"; 3 | import { SecretLintMessage } from "./types"; 4 | 5 | const lintManager = (() => { 6 | const _messages = new Map(); 7 | return { 8 | get(url: string) { 9 | return _messages.get(url); 10 | }, 11 | update(url: string, messages: SecretLintMessage[]) { 12 | _messages.set(url, messages); 13 | }, 14 | add(url: string, messages: SecretLintMessage[]) { 15 | const currentMessages = _messages.get(url) ?? []; 16 | _messages.set(url, currentMessages.concat(messages)); 17 | }, 18 | delete(url: string) { 19 | _messages.delete(url); 20 | }, 21 | clear() { 22 | _messages.clear(); 23 | } 24 | }; 25 | })(); 26 | 27 | onMessage("lint-messages", async (message) => { 28 | const { data, sender } = message; 29 | const tab = await browser.tabs.get(sender.tabId); 30 | if (isInternalEndpoint(sender) && tab && tab.url) { 31 | lintManager.add(tab.url, data as SecretLintMessage[]); 32 | } 33 | return {}; 34 | }); 35 | onMessage("pull-messages", async ({ sender }) => { 36 | const tab = await browser.tabs.get(sender.tabId); 37 | if (!isInternalEndpoint(sender)) { 38 | throw new Error("no support sender"); 39 | } 40 | if (!tab) { 41 | throw new Error("not found sender tab"); 42 | } 43 | if (!tab.url) { 44 | throw new Error("not found sender tab"); 45 | } 46 | return lintManager.get(tab.url) ?? []; 47 | }); 48 | 49 | onMessage("clear-messages", async ({ sender }) => { 50 | if (isInternalEndpoint(sender)) { 51 | lintManager.clear(); 52 | } 53 | return {}; 54 | }); 55 | 56 | browser.webNavigation.onCommitted.addListener((details) => { 57 | lintManager.delete(details.url); 58 | }); 59 | -------------------------------------------------------------------------------- /app/scripts/settings/CodeEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useCallback, Suspense, lazy } from "react"; 2 | import CodeMirror, * as codemirror from "codemirror"; 3 | import "codemirror/lib/codemirror.css"; 4 | 5 | const Controlled = lazy(async () => { 6 | await import("codemirror/addon/selection/active-line"); 7 | await import("codemirror/addon/fold/foldcode"); 8 | await import("codemirror/addon/fold/foldgutter"); 9 | await import("codemirror/mode/javascript/javascript" as string); 10 | await import("codemirror/mode/css/css" as string); 11 | await import("codemirror/mode/xml/xml" as string); 12 | const { Controlled } = await import("react-codemirror2"); 13 | return { 14 | default: Controlled 15 | }; 16 | }); 17 | 18 | export type CodeEditorProps = { 19 | value: string; 20 | readonly?: boolean; 21 | lang: "json"; 22 | onChange: (value: string) => void; 23 | }; 24 | export const CodeEditor = ({ 25 | value, 26 | readonly, 27 | lang, 28 | onChange, 29 | ...props 30 | }: Omit, "onChange"> & CodeEditorProps) => { 31 | const handleChange = useCallback( 32 | (_editor: CodeMirror.Editor, _data: CodeMirror.EditorChange, value: string) => { 33 | onChange(value); 34 | }, 35 | [onChange] 36 | ); 37 | 38 | const codeMirrorOption: codemirror.EditorConfiguration = useMemo(() => { 39 | return { 40 | readOnly: readonly, 41 | mode: lang === "json" ? "javascript" : lang, 42 | autoCloseBrackets: true, 43 | lineNumbers: true, 44 | lineWrapping: true, 45 | tabindex: 2, 46 | indentUnit: 2, 47 | indentWithTabs: true, 48 | tabSize: 2, 49 | inputStyle: "textarea", 50 | cursorHeight: 1, 51 | styleActiveLine: true, 52 | nonEmpty: true, 53 | foldGutter: true, 54 | gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], 55 | showInvisibles: true, 56 | ...(lang === "json" 57 | ? { 58 | json: true 59 | } 60 | : {}) 61 | }; 62 | }, [lang, readonly]); 63 | 64 | return ( 65 |
66 | ...loading
}> 67 | 68 | 69 | 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '28 21 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v3 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /app/scripts/dev_tools_panel/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { sendMessage } from "webext-bridge"; 3 | import React from "react"; 4 | import type { SecretLintMessage } from "../types"; 5 | import "./base.css"; 6 | import "./App.css"; 7 | 8 | const useSecretlint = () => { 9 | const [messages, setMessages] = useState([]); 10 | useEffect(() => { 11 | const update = async () => { 12 | const messages = await sendMessage("pull-messages", {}, "background"); 13 | setMessages(messages as SecretLintMessage[]); 14 | }; 15 | const timerId = setInterval(update, 500); 16 | update(); 17 | return () => { 18 | clearInterval(timerId); 19 | }; 20 | }, []); 21 | return messages; 22 | }; 23 | export const MessageList = () => { 24 | const messages = useSecretlint(); 25 | if (messages.length === 0) { 26 | return ( 27 |
28 |

Not found secrets in this page.

29 |
30 | ); 31 | } 32 | return ( 33 |
34 | {messages.map((message, i) => { 35 | return ( 36 |
37 |
38 |
39 |

40 | 🔑{" "} 41 | {message.docsUrl ? ( 42 | 48 | {message.ruleId} 49 | 50 | ) : ( 51 | {message.ruleId} 52 | )}{" "} 53 | {message.message} 54 |

55 |

56 | 📍{" "} 57 | 58 | {message.url} 59 | 60 |

61 |
62 |
63 | Details 64 |
{JSON.stringify(message, null, 4)}
65 |
66 |
67 |
68 | ); 69 | })} 70 |
71 | ); 72 | }; 73 | export const App = () => { 74 | return ( 75 |
76 | 77 |
78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webextension", 3 | "version": "1.3.3", 4 | "description": "Check secrets in your request/response using secretlint.", 5 | "keywords": [ 6 | "webextension", 7 | "secretlint", 8 | "secrets", 9 | "credentials", 10 | "security" 11 | ], 12 | "homepage": "https://github.com/secretlint/webextension", 13 | "bugs": { 14 | "url": "https://github.com/secretlint/webextension/issues" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/secretlint/webextension.git" 19 | }, 20 | "license": "MIT", 21 | "author": "azu", 22 | "sideEffects": false, 23 | "main": "lib/webextension.js", 24 | "module": "module/webextension.js", 25 | "types": "lib/webextension.d.ts", 26 | "directories": { 27 | "lib": "lib", 28 | "test": "test" 29 | }, 30 | "files": [ 31 | "bin/", 32 | "lib/", 33 | "module/", 34 | "src/" 35 | ], 36 | "scripts": { 37 | "test": "yarn run type-check && yarn run lint", 38 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,css}\"", 39 | "prepare": "git config --local core.hooksPath .githooks", 40 | "dev": "webextension-toolbox dev", 41 | "build": "webextension-toolbox build", 42 | "type-check": "tsc", 43 | "lint": "eslint .", 44 | "lint:fix": "eslint . --fix", 45 | "clean": "rimraf lib/ module/", 46 | "update:schema": "create-validator-ts ./app/scripts/settings/SettingSchema.ts", 47 | "prepublishOnly": "npm run clean && npm run build", 48 | "versionup:patch": "cd app && npm version patch", 49 | "versionup:minor": "cd app && npm version minor", 50 | "versionup:major": "cd app && npm version major" 51 | }, 52 | "lint-staged": { 53 | "*.{js,jsx,ts,tsx,css}": [ 54 | "prettier --write" 55 | ] 56 | }, 57 | "prettier": { 58 | "singleQuote": false, 59 | "printWidth": 120, 60 | "tabWidth": 4, 61 | "trailingComma": "none" 62 | }, 63 | "devDependencies": { 64 | "@types/codemirror": "^5.60.7", 65 | "@types/har-format": "^1.2.10", 66 | "@types/jsesc": "^3.0.1", 67 | "@types/mocha": "^9.1.1", 68 | "@types/node": "^16.18.21", 69 | "@types/react": "^17.0.44", 70 | "@types/react-dom": "^17.0.16", 71 | "@typescript-eslint/eslint-plugin": "^4.33.0", 72 | "@typescript-eslint/parser": "^4.33.0", 73 | "create-validator-ts": "^2.0.0", 74 | "css-loader": "^6.7.3", 75 | "eslint": "^7.32.0", 76 | "eslint-plugin-react": "^7.32.2", 77 | "eslint-plugin-react-hooks": "^4.6.0", 78 | "lint-staged": "^11.2.6", 79 | "mocha": "^9.2.2", 80 | "node-polyfill-webpack-plugin": "^1.1.4", 81 | "prettier": "^2.3.2", 82 | "rimraf": "^3.0.2", 83 | "style-loader": "^3.3.2", 84 | "ts-loader": "^9.4.2", 85 | "ts-node": "^10.9.1", 86 | "ts-node-test-register": "^10.0.0", 87 | "typescript": "^4.3.5", 88 | "webextension-toolbox": "^4.0.3", 89 | "webpack-watched-glob-entries-plugin": "^2.2.6" 90 | }, 91 | "dependencies": { 92 | "@rjsf/core": "^3.2.1", 93 | "@secretlint/core": "^3.3.0", 94 | "@secretlint/secretlint-rule-pattern": "^3.3.0", 95 | "@secretlint/secretlint-rule-preset-recommend": "^3.3.0", 96 | "codemirror": "^5.65.12", 97 | "react": "^17.0.2", 98 | "react-codemirror2": "^7.2.1", 99 | "react-dom": "^17.0.2", 100 | "webext-bridge": "^4.1.1", 101 | "webextension-polyfill-ts": "^0.26.0" 102 | }, 103 | "resolutions": { 104 | "webpack": "5.76.3" 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /app/scripts/dev_tools.ts: -------------------------------------------------------------------------------- 1 | import { browser, DevtoolsNetwork } from "webextension-polyfill-ts"; 2 | import type { Request } from "har-format"; 3 | import { lintContent } from "./secretlint/lint"; 4 | import { sendMessage } from "webext-bridge"; 5 | import { SecretLintMessage } from "./types"; 6 | import { SettingSchema } from "./settings/SettingSchema"; 7 | import { SCHEMA } from "./settings/SettingSchema.validator"; 8 | import { RULES_DEFAULT_ALLOWS } from "./secretlint/rule.allows"; 9 | 10 | const headersToEnv = (headers: Request["headers"]): string => { 11 | return headers 12 | .map((header) => { 13 | return `${header.name}=${header.value}`; 14 | }) 15 | .join("\n"); 16 | }; 17 | const lintContentAndSend = async ({ 18 | url, 19 | content, 20 | setting 21 | }: { 22 | url: string; 23 | content: string; 24 | setting: SettingSchema & { allows: string[] }; 25 | }): Promise => { 26 | const result = await lintContent({ 27 | content, 28 | url: url, 29 | allows: setting.allows 30 | }); 31 | if (result.messages.length === 0) { 32 | return []; 33 | } 34 | const lintMessages: SecretLintMessage[] = result.messages.map((message) => { 35 | const sliceContent = content.slice( 36 | Math.max(message.range[0] - setting.sliceBefore, 0), 37 | Math.min(message.range[1] + setting.sliceAfter, content.length) 38 | ); 39 | return { 40 | ...message, 41 | sliceContent, 42 | url 43 | }; 44 | }); 45 | // send lint results to background page for pulling these from devtools panel 46 | await sendMessage("lint-messages", lintMessages, "background"); 47 | return lintMessages; 48 | }; 49 | (async function main() { 50 | await browser.devtools.panels.create("Secretlint", "/images/icon-192.png", "/pages/dev_tools_panel.html"); 51 | const storage = await browser.storage.local.get(["settings", "rule.allows"]); 52 | const setting = { 53 | sliceBefore: SCHEMA.definitions.SettingSchema.properties.sliceBefore.default, 54 | sliceAfter: SCHEMA.definitions.SettingSchema.properties.sliceAfter.default, 55 | enableConsoleIntegration: SCHEMA.definitions.SettingSchema.properties.enableConsoleIntegration.default, 56 | allows: storage["rule.allows"] ?? RULES_DEFAULT_ALLOWS, 57 | ...storage?.settings 58 | } as SettingSchema & { 59 | allows: string[]; 60 | }; 61 | const onRequestFinished = async ( 62 | request: DevtoolsNetwork.Request & { request?: Request; serverIPAddress?: string } 63 | ) => { 64 | const harRequest = request.request as Request; 65 | const [content, mimeType] = await request.getContent(); 66 | const isBinary = mimeType === "base64"; 67 | const url = harRequest.url; 68 | const headerLinting = 69 | Object.keys(harRequest.headers).length > 0 70 | ? lintContentAndSend({ 71 | url: "request-header:" + url, 72 | content: headersToEnv(harRequest.headers), 73 | setting 74 | }) 75 | : Promise.resolve([]); 76 | const contentLinting = 77 | !isBinary && content 78 | ? lintContentAndSend({ 79 | url, 80 | content, 81 | setting 82 | }) 83 | : Promise.resolve([]); 84 | const messages = (await Promise.all([headerLinting, contentLinting])).flat(); 85 | if (setting.enableConsoleIntegration && messages.length > 0) { 86 | sendMessage("content-script:log-lint-messages", messages, "content-script"); 87 | } 88 | }; 89 | browser.devtools.network.onRequestFinished.addListener(onRequestFinished); 90 | })(); 91 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | *.zip 3 | ### https://raw.github.com/github/gitignore/d2c1bb2b9c72ead618c9f6a48280ebc7a8e0dff6/Node.gitignore 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 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 | 87 | ### https://raw.github.com/github/gitignore/d2c1bb2b9c72ead618c9f6a48280ebc7a8e0dff6/Global/JetBrains.gitignore 88 | 89 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 90 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 91 | 92 | # User-specific stuff 93 | .idea/**/workspace.xml 94 | .idea/**/tasks.xml 95 | .idea/**/usage.statistics.xml 96 | .idea/**/dictionaries 97 | .idea/**/shelf 98 | 99 | # Generated files 100 | .idea/**/contentModel.xml 101 | 102 | # Sensitive or high-churn files 103 | .idea/**/dataSources/ 104 | .idea/**/dataSources.ids 105 | .idea/**/dataSources.local.xml 106 | .idea/**/sqlDataSources.xml 107 | .idea/**/dynamic.xml 108 | .idea/**/uiDesigner.xml 109 | .idea/**/dbnavigator.xml 110 | 111 | # Gradle 112 | .idea/**/gradle.xml 113 | .idea/**/libraries 114 | 115 | # Gradle and Maven with auto-import 116 | # When using Gradle or Maven with auto-import, you should exclude module files, 117 | # since they will be recreated, and may cause churn. Uncomment if using 118 | # auto-import. 119 | # .idea/modules.xml 120 | # .idea/*.iml 121 | # .idea/modules 122 | 123 | # CMake 124 | cmake-build-*/ 125 | 126 | # Mongo Explorer plugin 127 | .idea/**/mongoSettings.xml 128 | 129 | # File-based project format 130 | *.iws 131 | 132 | # IntelliJ 133 | out/ 134 | 135 | # mpeltonen/sbt-idea plugin 136 | .idea_modules/ 137 | 138 | # JIRA plugin 139 | atlassian-ide-plugin.xml 140 | 141 | # Cursive Clojure plugin 142 | .idea/replstate.xml 143 | 144 | # Crashlytics plugin (for Android Studio and IntelliJ) 145 | com_crashlytics_export_strings.xml 146 | crashlytics.properties 147 | crashlytics-build.properties 148 | fabric.properties 149 | 150 | # Editor-based Rest Client 151 | .idea/httpRequests 152 | 153 | # Android studio 3.1+ serialized cache file 154 | .idea/caches/build_file_checksums.ser 155 | 156 | 157 | ### https://raw.github.com/github/gitignore/d2c1bb2b9c72ead618c9f6a48280ebc7a8e0dff6/Global/VisualStudioCode.gitignore 158 | 159 | .vscode/* 160 | !.vscode/settings.json 161 | !.vscode/tasks.json 162 | !.vscode/launch.json 163 | !.vscode/extensions.json 164 | 165 | 166 | # Build files 167 | /lib 168 | /module 169 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | *.zip 3 | ### https://raw.github.com/github/gitignore/d2c1bb2b9c72ead618c9f6a48280ebc7a8e0dff6/Node.gitignore 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 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 | 87 | ### https://raw.github.com/github/gitignore/d2c1bb2b9c72ead618c9f6a48280ebc7a8e0dff6/Global/JetBrains.gitignore 88 | 89 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 90 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 91 | 92 | # User-specific stuff 93 | .idea/**/workspace.xml 94 | .idea/**/tasks.xml 95 | .idea/**/usage.statistics.xml 96 | .idea/**/dictionaries 97 | .idea/**/shelf 98 | 99 | # Generated files 100 | .idea/**/contentModel.xml 101 | 102 | # Sensitive or high-churn files 103 | .idea/**/dataSources/ 104 | .idea/**/dataSources.ids 105 | .idea/**/dataSources.local.xml 106 | .idea/**/sqlDataSources.xml 107 | .idea/**/dynamic.xml 108 | .idea/**/uiDesigner.xml 109 | .idea/**/dbnavigator.xml 110 | 111 | # Gradle 112 | .idea/**/gradle.xml 113 | .idea/**/libraries 114 | 115 | # Gradle and Maven with auto-import 116 | # When using Gradle or Maven with auto-import, you should exclude module files, 117 | # since they will be recreated, and may cause churn. Uncomment if using 118 | # auto-import. 119 | # .idea/modules.xml 120 | # .idea/*.iml 121 | # .idea/modules 122 | 123 | # CMake 124 | cmake-build-*/ 125 | 126 | # Mongo Explorer plugin 127 | .idea/**/mongoSettings.xml 128 | 129 | # File-based project format 130 | *.iws 131 | 132 | # IntelliJ 133 | out/ 134 | 135 | # mpeltonen/sbt-idea plugin 136 | .idea_modules/ 137 | 138 | # JIRA plugin 139 | atlassian-ide-plugin.xml 140 | 141 | # Cursive Clojure plugin 142 | .idea/replstate.xml 143 | 144 | # Crashlytics plugin (for Android Studio and IntelliJ) 145 | com_crashlytics_export_strings.xml 146 | crashlytics.properties 147 | crashlytics-build.properties 148 | fabric.properties 149 | 150 | # Editor-based Rest Client 151 | .idea/httpRequests 152 | 153 | # Android studio 3.1+ serialized cache file 154 | .idea/caches/build_file_checksums.ser 155 | 156 | 157 | ### https://raw.github.com/github/gitignore/d2c1bb2b9c72ead618c9f6a48280ebc7a8e0dff6/Global/VisualStudioCode.gitignore 158 | 159 | .vscode/* 160 | !.vscode/settings.json 161 | !.vscode/tasks.json 162 | !.vscode/launch.json 163 | !.vscode/extensions.json 164 | 165 | 166 | # Build files 167 | /lib 168 | /module 169 | 170 | # Local Netlify folder 171 | .netlify -------------------------------------------------------------------------------- /app/scripts/settings/App.tsx: -------------------------------------------------------------------------------- 1 | import Form, { ISubmitEvent } from "@rjsf/core"; 2 | import React, { useCallback, useEffect, useState } from "react"; 3 | import { SCHEMA } from "./SettingSchema.validator"; 4 | import { browser } from "webextension-polyfill-ts"; 5 | import { SettingSchema } from "./SettingSchema"; 6 | import { RULE_DEFAULT_PATTERNS } from "../secretlint/rule.patterns"; 7 | import { CodeEditor } from "./CodeEditor"; 8 | import { RULES_DEFAULT_ALLOWS } from "../secretlint/rule.allows"; 9 | 10 | const useStorageLocal = (name: string, initialValue?: V) => { 11 | const [state, setState] = useState(initialValue); 12 | const get = useCallback(() => { 13 | return browser.storage.local.get([name]).then((storage) => { 14 | return storage[name]; 15 | }); 16 | }, [name]); 17 | const set = useCallback( 18 | (value: V) => { 19 | return browser.storage.local.set({ [name]: value }); 20 | }, 21 | [name] 22 | ); 23 | const load = useCallback(async () => { 24 | const initial = await get(); 25 | if (initial) { 26 | setState(initial); 27 | } 28 | }, [get]); 29 | 30 | useEffect(() => { 31 | load(); 32 | }, [load]); 33 | return [state, set] as const; 34 | }; 35 | type Pattern = { name: string; pattern: string }; 36 | type Patterns = Pattern[]; 37 | export const useApp = () => { 38 | const [settings, setSettings] = useStorageLocal>("settings"); 39 | const [patterns, setPatterns] = useStorageLocal("rule.patterns", RULE_DEFAULT_PATTERNS); 40 | const [allows, setAllows] = useStorageLocal("rule.allows", RULES_DEFAULT_ALLOWS); 41 | const [patternValue, setPatternValue] = useState(JSON.stringify(patterns, null, 2)); 42 | const [allowsValue, setAllowsValue] = useState(JSON.stringify(allows, null, 2)); 43 | useEffect(() => { 44 | setPatternValue(JSON.stringify(patterns, null, 2)); 45 | }, [patterns]); 46 | useEffect(() => { 47 | setAllowsValue(JSON.stringify(allows, null, 2)); 48 | }, [allows]); 49 | const onSaveSettings = useCallback( 50 | (event: ISubmitEvent>) => { 51 | setSettings(event.formData); 52 | }, 53 | [setSettings] 54 | ); 55 | const onChangePatterns = useCallback( 56 | (newValue: string) => { 57 | try { 58 | setPatternValue(newValue); 59 | const json = JSON.parse(newValue) as Patterns; 60 | if (!Array.isArray(json)) { 61 | return; 62 | } 63 | setPatterns(json); 64 | } catch { 65 | // nope 66 | } 67 | }, 68 | [setPatterns] 69 | ); 70 | const onChangeAllows = useCallback( 71 | (newValue: string) => { 72 | try { 73 | setAllowsValue(newValue); 74 | const json = JSON.parse(newValue) as string[]; 75 | if (!Array.isArray(json)) { 76 | return; 77 | } 78 | setAllows(json); 79 | } catch { 80 | // nope 81 | } 82 | }, 83 | [setAllows] 84 | ); 85 | 86 | const resetConfig = useCallback(() => { 87 | if (confirm("Reset all config. OK?")) { 88 | browser.storage.local.clear(); 89 | } 90 | }, []); 91 | 92 | return [ 93 | { settings, patterns, allows, patternValue, allowsValue }, 94 | { onSaveSettings, onChangePatterns, onChangeAllows, resetConfig } 95 | ] as const; 96 | }; 97 | export const App = () => { 98 | const [{ settings, patternValue, allowsValue }, { onSaveSettings, onChangePatterns, onChangeAllows, resetConfig }] = 99 | useApp(); 100 | return ( 101 |
107 |
117 | 118 |
119 |

Allows patterns

120 |

121 | If match following patterns, just ignore it. For more details, see{" "} 122 | 123 | Document📝 124 | 125 |

126 | 127 |

Disallows patterns

128 |

129 | If match following patterns, report it as error. For more details, see{" "} 130 | 131 | Document📝 132 | 133 |

134 | 135 |

Danger Zone

136 | 137 |
138 | ); 139 | }; 140 | -------------------------------------------------------------------------------- /app/scripts/secretlint/rule.patterns.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This patterns are based on https://github.com/secretlint/secretlint/tree/master/packages/%40secretlint/secretlint-rule-pattern 3 | * 4 | * Each pattern should have following properties. If match the pattern, report it as error. 5 | * 6 | * name: pattern name 7 | * pattern: string or regexp-like string(/pattern/) 8 | * 9 | */ 10 | export const RULE_DEFAULT_PATTERNS = [ 11 | { 12 | name: "Artifactory API Token", 13 | pattern: '/(?:\\s|=|:|"|^)AKC[a-zA-Z0-9]{10,}/' 14 | }, 15 | { 16 | name: "Artifactory Password", 17 | pattern: '/(?:\\s|=|:|"|^)AP[\\dABCDEF][a-zA-Z0-9]{8,}/' 18 | }, 19 | { 20 | name: "AWS Client ID", 21 | pattern: "/(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}/" 22 | }, 23 | { 24 | name: "AWS MWS Key", 25 | pattern: "/amzn\\.mws\\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/" 26 | }, 27 | { 28 | name: "AWS Secret Key", 29 | pattern: "/(?i)aws(.{0,20})?(?-i)['\"][0-9a-zA-Z\\/+]{40}['\"]/" 30 | }, 31 | { 32 | name: "Cloudinary Basic Auth", 33 | pattern: "/cloudinary:\\/\\/[0-9]{15}:[0-9A-Za-z]+@[a-z]+/" 34 | }, 35 | { 36 | name: "Facebook Access Token", 37 | pattern: "/EAACEdEose0cBA[0-9A-Za-z]{32,64}/" 38 | }, 39 | { 40 | name: "Facebook Client ID", 41 | pattern: "/(?i)(facebook|fb)(.{0,20})?['\"][0-9]{13,17}['\"]/" 42 | }, 43 | { 44 | name: "Facebook Oauth", 45 | pattern: "/[f|F][a|A][c|C][e|E][b|B][o|O][o|O][k|K](.{0,20}['|\"][0-9a-f]{32}['|\"]/" 46 | }, 47 | { 48 | name: "Facebook Secret Key", 49 | pattern: "/(?i)(facebook|fb)(.{0,20})?(?-i)['\"][0-9a-f]{32}['\"]/" 50 | }, 51 | { 52 | name: "Github", 53 | pattern: "/(?i)github(.{0,20})?(?-i)['\"][0-9a-zA-Z]{35,40}/" 54 | }, 55 | { 56 | name: "Google Cloud Platform API Key", 57 | pattern: "/(?i)(google|gcp|youtube|drive|yt)(.{0,20})?['\"][AIza[0-9a-z\\-_]{35}]['\"]/" 58 | }, 59 | { 60 | name: "Google Drive Oauth", 61 | pattern: "/[0-9]+-[0-9A-Za-z_]{32}\\.apps\\.googleusercontent\\.com/" 62 | }, 63 | { 64 | name: "Google Gmail Oauth", 65 | pattern: "/[0-9]+-[0-9A-Za-z_]{32}\\.apps\\.googleusercontent\\.com/" 66 | }, 67 | { 68 | name: "Google Oauth Access Token", 69 | pattern: "/ya29\\.[0-9A-Za-z\\-_]+/" 70 | }, 71 | { 72 | name: "Google Youtube Oauth", 73 | pattern: "/[0-9]+-[0-9A-Za-z_]{32}\\.apps\\.googleusercontent\\.com/" 74 | }, 75 | { 76 | name: "Heroku API Key", 77 | pattern: "/[h|H][e|E][r|R][o|O][k|K][u|U].{0,30}[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/" 78 | }, 79 | { 80 | name: "IPv4", 81 | pattern: "/['\"](25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}['\"]/" 82 | }, 83 | // { 84 | // name: "IPv6", 85 | // pattern: 86 | // "/(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/" 87 | // }, 88 | { 89 | name: "LinkedIn Client ID", 90 | pattern: "/(?i)linkedin(.{0,20})?(?-i)['\"][0-9a-z]{12}['\"]/" 91 | }, 92 | { 93 | name: "LinkedIn Secret Key", 94 | pattern: "/(?i)linkedin(.{0,20})?['\"][0-9a-z]{16}['\"]/" 95 | }, 96 | { 97 | name: "Mailchamp API Key", 98 | pattern: "/[0-9a-f]{32}-us[0-9]{1,2}/" 99 | }, 100 | { 101 | name: "Mailgun API Key", 102 | pattern: "/key-[0-9a-zA-Z]{32}/" 103 | }, 104 | { 105 | name: "Mailto:", 106 | pattern: "/(?<=mailto:)[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9.-]+/" 107 | }, 108 | // { 109 | // name: "MD5 Hash", 110 | // pattern: "/[a-f0-9]{32}/" 111 | // }, 112 | { 113 | name: "Picatic API Key", 114 | pattern: "/sk_live_[0-9a-z]{32}/" 115 | }, 116 | { 117 | name: "Slack Token", 118 | pattern: "/xox[baprs]-([0-9a-zA-Z]{10,48})?/" 119 | }, 120 | { 121 | name: "Slack Webhook", 122 | pattern: "/https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8}/[a-zA-Z0-9_]{24}/" 123 | }, 124 | { 125 | name: "Stripe API Key", 126 | pattern: "/(?:r|s)k_live_[0-9a-zA-Z]{24}/" 127 | }, 128 | { 129 | name: "Square Access Token", 130 | pattern: "/sqOatp-[0-9A-Za-z\\-_]{22}/" 131 | }, 132 | { 133 | name: "Square Oauth Secret", 134 | pattern: "/sq0csp-[ 0-9A-Za-z\\-_]{43}/" 135 | }, 136 | { 137 | name: "Twilio API Key", 138 | pattern: "/SK[0-9a-fA-F]{32}/" 139 | }, 140 | { 141 | name: "Twitter Client ID", 142 | pattern: "/(?i)twitter(.{0,20})?['\"][0-9a-z]{18,25}/" 143 | }, 144 | { 145 | name: "Twitter Oauth", 146 | pattern: "/[t|T][w|W][i|I][t|T][t|T][e|E][r|R].{0,30}['\"\\s][0-9a-zA-Z]{35,44}['\"\\s]/" 147 | }, 148 | { 149 | name: "Twitter Secret Key", 150 | pattern: "/(?i)twitter(.{0,20})?['\"][0-9a-z]{35,44}/" 151 | }, 152 | { 153 | name: "Vault Token", 154 | pattern: "/['\"\\s][sb]\\.[a-zA-Z0-9]{24}['\"\\s]/" 155 | }, 156 | // Request Header 157 | { 158 | name: "Authorization Basic", 159 | pattern: `/Authorization=basic [a-zA-Z0-9_\\-:\\.=]+/i`, 160 | isRequestHeader: true 161 | }, 162 | { 163 | name: "Authorization Bearer", 164 | pattern: `/Authorization=bearer [a-zA-Z0-9_\\-:\\.=]+/i`, 165 | isRequestHeader: true 166 | } 167 | ]; 168 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Secretlint WebExtension 2 | 3 | [Secretlint](https://github.com/secretlint/secretlint) founds credentials that are included in your request/response. 4 | 5 | - Firefox: 6 | - Chrome: 7 | - Install from Source: See [Development](#Development) section 8 | 9 | This Web Extension integrate [secretlint](https://github.com/secretlint/secretlint) with browser's devTools. 10 | 11 | ![screenshot](docs/screenshot.png) 12 | 13 | :memo: If you want to run secretelint as command line tools, please see [secretlint/secretlint](https://github.com/secretlint/secretlint). 14 | 15 | ## Features 16 | 17 | - Check that request/response includes credentials 18 | - DevTools integration 19 | - Output found credentials to "Console" panel(option) 20 | - Output found credentials to "Secretlint" panel 21 | 22 | ## Permissions 23 | 24 | This extension requires following permissions 25 | 26 | - `""` 27 | - It is used for extending devTools and "Console Integration" 28 | - [devtools API](https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/Extending_the_developer_tools) requires this permission 29 | - "Console Integration" uses [content_scripts](https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/manifest.json/content_scripts). Content Scripts require this permission 30 | - Related issue: [Reduce to use content scripts · Issue #5](https://github.com/secretlint/webextension/issues/5) 31 | - `"webNavigation"` 32 | - It is used for clearing lint messages when move pages 33 | - `"storage"` 34 | - It is used for [user config](#Config) 35 | 36 | `permissions` is defeind in [manifest.json](./app/manifest.json). 37 | 38 | 📝 Other Notes 39 | 40 | **In Memory Process** 41 | 42 | This extension is written by JavaScript and It do not send your request/response to another server. 43 | All process is done in memory. 44 | 45 | **Scan timing** 46 | 47 | This exntension only scans secrents during you open developer tools. 48 | This limitation come from [devtools API](https://developer.mozilla.org/ja/docs/Mozilla/Add-ons/WebExtensions/Extending_the_developer_tools). 49 | 50 | If you close the devTools, this extension does not scan any request/response. 51 | 52 | ## Motivation 53 | 54 | Everyone makes mistakes. 55 | 56 | A developer sometimes expose own credentials like OAuth token in a website accidentally. 57 | 58 | [secretlint](https://github.com/secretlint/secretlint) can found credentials in file. 59 | However, The exposed credentials come from environments variables or Database, so These are not embed in a file. 60 | 61 | We want to found these exposed credentials. 62 | 63 | Security researcher use proxy software like [Burp Suite](https://portswigger.net/burp), but web developer use DevTools instead of it. 64 | 65 | Secretlint WebExtension integrate to DevTools in Chrome/Firefox. 66 | This extension help web developer to notice exposed credential. 67 | 68 | ## Install 69 | 70 | - Firefox: 71 | - Chrome: 72 | 73 | ## Usage 74 | 75 | 1. Open browser's Developer Tools 76 | 2. ✅ Disable Cache 77 | 3. Reload page and secretlint report found credentials in your request/response. 78 | 79 | You can check the behavior using demo site: 80 | 81 | - Demo: 82 | 83 | ## Built-in rules 84 | 85 | This Web Extension use [@secretlint/secretlint-rule-preset-recommend](https://github.com/secretlint/secretlint/tree/master/packages/@secretlint/secretlint-rule-preset-recommend/) and built-in disallow patterns. 86 | 87 | ## Config 88 | 89 | You can configure the option of secretlint extension. 90 | 91 | - Click "Secretlint" icon on menu 92 | - Or, See This extension's "Settings" page 93 | 94 | ### Allow Patterns 95 | 96 | Allow patterns is an array of string or [RegExp-like String](https://github.com/textlint/regexp-string-matcher#regexp-like-string) (/pattern/). 97 | 98 | If you define following pattern, secretlint does not report it which is matched. 99 | 100 | ```ts 101 | [ 102 | "/NON_SECRETS/i", 103 | "1.1.1.1", 104 | "AKIAIOSFODNN7SECRETS", 105 | ] 106 | ``` 107 | 108 | Default patterns are defined in [rule.allows.ts](app/scripts/secretlint/rule.allows.ts). 109 | 110 | :memo: Prefer Allow patterns than Disallow patterns. 111 | 112 | ### Disallow Patterns 113 | 114 | You can add patterns and found your secrets. 115 | 116 | These patterns are based on [@secretlint/secretlint-rule-pattern](https://github.com/secretlint/secretlint/tree/master/packages/%40secretlint/secretlint-rule-pattern). 117 | Each pattern should have following properties. If match the pattern, report it as error. 118 | 119 | - name: pattern name 120 | - pattern: string or [RegExp-like String](https://github.com/textlint/regexp-string-matcher#regexp-like-string) (/pattern/) 121 | 122 | Default patterns are defined in [rule.patterns.ts](app/scripts/secretlint/rule.patterns.ts). 123 | 124 | ## Development 125 | 126 | Build this extension from source code: 127 | 128 | # Require Node.js and Yarn 129 | yarn install 130 | # Chrome 131 | yarn dev chrome 132 | # Firefox 133 | yarn dev firefox 134 | 135 | Load the built extension: 136 | 137 | - Firefox: open `about:debugging#/runtime/this-firefox` → Load from local 138 | - Chrome: open `chrome://extensions/` → Load from local 139 | 140 | ## Changelog 141 | 142 | See [Releases page](https://github.com/secretlint/webextension/releases). 143 | 144 | ## Running tests 145 | 146 | Install devDependencies and Run `npm test`: 147 | 148 | npm test 149 | 150 | ## Contributing 151 | 152 | Pull requests and stars are always welcome. 153 | 154 | For bugs and feature requests, [please create an issue](https://github.com/secretlint/webextension/issues). 155 | 156 | 1. Fork it! 157 | 2. Create your feature branch: `git checkout -b my-new-feature` 158 | 3. Commit your changes: `git commit -am 'Add some feature'` 159 | 4. Push to the branch: `git push origin my-new-feature` 160 | 5. Submit a pull request :D 161 | 162 | ## Author 163 | 164 | - azu: [GitHub](https://github.com/azu), [Twitter](https://twitter.com/azu_re) 165 | 166 | ## License 167 | 168 | MIT © azu 169 | --------------------------------------------------------------------------------