evsecrets
Version:
Detect environment variable secrets in your codebase before you push your code to GitHub.
482 lines • 18.3 kB
JavaScript
"use strict";
/**
* This is the primary implementation class in this library.
* Chris Joakim, 2025
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EnvScanner = void 0;
const os_1 = __importDefault(require("os"));
const util_1 = __importDefault(require("util"));
const fsWalk = __importStar(require("@nodelib/fs.walk"));
const DotEnvReader_1 = require("./DotEnvReader");
const FileUtil_1 = require("./FileUtil");
class EnvScanner {
constructor() {
this.fu = null;
this.envVarPatterns = null;
this.excludeFilePatterns = null;
this.excludeFileSuffixes = null;
this.allFilenames = null;
this.verbose = false;
this.tmpFileOutputs = false;
this.fu = new FileUtil_1.FileUtil();
this.allFilenames = Array();
try {
// first set these arrays to their defaults
this.envVarPatterns = this.defaultEnvVarPatterns();
this.excludeFilePatterns = this.defaultExcludeFilePatterns();
this.excludeFileSuffixes = this.defaultExcludeFileSuffixes();
// next try to read the '.evsecrets.json' configuration file
if (this.fu.fileExists(EnvScanner.CONFIG_FILE)) {
try {
let txt = this.fu.readTextFileSync(EnvScanner.CONFIG_FILE);
let obj = JSON.parse(txt);
this.envVarPatterns = obj['env_var_patterns'];
this.excludeFilePatterns = obj['exclude_file_patterns'];
this.excludeFileSuffixes = obj['exclude_file_suffixes'];
}
catch (error) {
console.error("error processing " + EnvScanner.CONFIG_FILE + ". using defaults.");
}
}
else {
console.log("file " + EnvScanner.CONFIG_FILE + " does not exist, using defaults.");
}
// verbose and debugging settings
this.verbose = this.cliArgPresent("--verbose");
this.tmpFileOutputs = this.cliArgPresent("--tmp-file-outputs");
if (this.verbose) {
console.log("verbose: " + this.verbose + ", tmpFileOutputs: " + this.tmpFileOutputs);
}
}
catch (error) {
console.error("evsecrets - error in constructor");
}
}
/**
* Return the version of this npm package.
*/
static version() {
return EnvScanner.VERSION;
}
/**
* Write a default '.evsecrets.json' file in the current directory.
*/
init() {
try {
let config = this.defaultConfig();
config['version'] = EnvScanner.version();
config['gen_timestamp'] = this.getFormattedTimestamp();
let outfile = EnvScanner.CONFIG_FILE;
this.fu.writeTextFileSync(outfile, JSON.stringify(config, null, 2));
if (this.verbose) {
console.log("file written: " + EnvScanner.CONFIG_FILE);
}
return true;
}
catch (error) {
console.log(error);
return false;
}
}
/**
* Return a timestamp string, for your local timezone, in 'YYYY-MM-DD HH:MM:SS' format.
* For example: '2025-04-15 13:04:39'
*/
getFormattedTimestamp() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const formattedTimestamp = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
return formattedTimestamp;
}
/**
* Return an array of the secret environment variable names per your defined patterns.
* This includes the environment variable names in the .env file, if present.
*/
secretEnvVars() {
let names = new Object();
let allEnvVarNames = Object.keys(process.env).sort();
for (let e = 0; e < allEnvVarNames.length; e++) {
let name = allEnvVarNames[e];
for (let i = 0; i < this.envVarPatterns.length; i++) {
let pattern = this.envVarPatterns[i];
if (name.includes(pattern)) {
names[name] = pattern;
}
}
}
// .env file logic
let dotEnvReader = new DotEnvReader_1.DotEnvReader();
let dotEnvVarNames = Object.keys(dotEnvReader.envVars).sort();
for (let e = 0; e < dotEnvVarNames.length; e++) {
let name = dotEnvVarNames[e];
for (let i = 0; i < this.envVarPatterns.length; i++) {
let pattern = this.envVarPatterns[i];
if (name.includes(pattern)) {
names[name] = pattern;
}
}
}
return Object.keys(names).sort();
}
/**
* Display a list of your secrets defined in your specified environment variables.
*/
secrets() {
let secretEnvVarNames = this.secretEnvVars();
let dotEnvReader = new DotEnvReader_1.DotEnvReader();
let msg = util_1.default.format('%s environment variables with secrets per your .evsecrets.json configuration:', secretEnvVarNames.length);
console.log(msg);
for (let e = 0; e < secretEnvVarNames.length; e++) {
let envvar = secretEnvVarNames[e];
let value = process.env[envvar];
// It's possible that an environment variable can be defined
// in one or two locations - the actual ENV, and the .env file.
console.log(envvar + ' --> ' + value + ' (environment)');
if (dotEnvReader.envVars.hasOwnProperty(envvar)) {
let value = dotEnvReader.envVars[envvar];
console.log(envvar + ' --> ' + value + ' (.env)');
}
}
}
/**
* Scan your codebase for the secrets defined in your environment variables,
* per the configuration in .evsecrets.json or the default configuration.
* Display the matches and line numbers in the terminal output.
*/
scan(codebaseRootDir = null, silent = false) {
let results = new Array();
try {
let secrets = new Array();
let envVars = this.secretEnvVars();
let dotEnvReader = new DotEnvReader_1.DotEnvReader();
let counter = 0;
// first, collect the secrets from either ENV and/or .env
for (let i = 0; i < envVars.length; i++) {
let envvar = envVars[i];
if (envvar in process.env) {
let secret = process.env[envvar]; // as string;
if (!secrets.includes(secret)) {
secrets.push(secret);
if (this.verbose) {
console.log("adding secret " + secret + " from ENV " + envvar);
}
}
}
if (dotEnvReader.envVars.hasOwnProperty(envvar)) {
let secret = dotEnvReader.envVars[envvar];
if (!secrets.includes(secret)) {
secrets.push(secret);
if (this.verbose) {
console.log("adding secret " + secret + " from .env " + envvar);
}
}
}
}
// next, scan the files for the identified secrets
let codebaseFilenames = this.filteredFilenamesList(codebaseRootDir);
for (let i = 0; i < codebaseFilenames.length; i++) {
let filename = codebaseFilenames[i];
let lines = this.fu.readTextFileAsLinesSync(filename);
let lineNum = 0;
for (let l = 0; l < lines.length; l++) {
let line = lines[l];
lineNum++;
for (let s = 0; s < secrets.length; s++) {
let secret = secrets[s];
if (line.includes(secret)) {
counter++;
let msg = '--- ' + counter;
results.push(msg);
if (!silent) {
console.log(msg);
}
msg = util_1.default.format('WARNING: Secret found at line %s of file %s', lineNum, filename);
results.push(msg);
if (!silent) {
console.log(msg);
}
msg = "content: " + line;
results.push(msg);
if (!silent) {
console.log(msg);
}
}
}
}
}
}
catch (error) {
console.log(error);
}
return results;
}
/**
* Print the filteredFilenamesList on the console/terminal.
*/
files() {
let filesList = this.filteredFilenamesList();
for (let i = 0; i < filesList.length; i++) {
console.log(filesList[i]);
}
}
/**
* Return a filtered list of files to scan for secrets.
*/
filteredFilenamesList(codebaseRootDir = null) {
this.allFilenames = this.walkFs(codebaseRootDir);
let filteredFilenames = new Object();
for (let i = 0; i < this.allFilenames.length; i++) {
let filename = this.allFilenames[i];
if (this.includeThisFile(filename)) {
filteredFilenames[filename] = 1;
}
}
if (this.verbose) {
console.log("filteredFilenamesList length: " + Object.keys(filteredFilenames).length);
}
if (this.tmpFileOutputs) {
try {
let outfile = "tmp/evsecrets-filteredFilenamesList.json";
this.fu.writeTextFileSync(outfile, JSON.stringify(filteredFilenames, null, 2));
console.log("file written: " + outfile);
}
catch (error) {
console.error(error);
}
}
return Object.keys(filteredFilenames).sort();
}
/**
* Recursively walk the filesystem from the given codebaseRootDir.
* Return an unfiltered list of all of the files found; not directories.
*/
walkFs(codebaseRootDir = null) {
let files = Array();
if (codebaseRootDir === null) {
codebaseRootDir = process.cwd();
}
let walkSettings = {};
walkSettings['stats'] = false;
walkSettings['followSymbolicLinks'] = false;
// This functionality is implemented with the '@nodelib/fs.walk' library
const entries = fsWalk.walkSync(codebaseRootDir, walkSettings);
for (let i = 0; i < entries.length; i++) {
let entry = entries[i];
// See https://nodejs.org/api/fs.html#direntisfile
if (entry.dirent.isFile()) {
files.push(entry.path);
}
}
if (this.verbose) {
console.log("walkFs length: " + files.length);
}
if (this.tmpFileOutputs) {
try {
let outfile = "tmp/evsecrets-walkFs.json";
this.fu.writeTextFileSync(outfile, JSON.stringify(files, null, 2));
console.log("file written: " + outfile);
}
catch (error) {
console.error(error);
}
}
return files;
}
/**
* Return a boolean indicating if the given filename should be included
* in the scanning process, based on the filename patterns and filename
* suffixes in the configuration.
*/
includeThisFile(filename) {
let result = true;
for (let i = 0; i < this.excludeFilePatterns.length; i++) {
let pattern = this.excludeFilePatterns[i];
if (filename.includes(pattern)) {
result = false;
}
}
for (let i = 0; i < this.excludeFileSuffixes.length; i++) {
let suffix = this.excludeFileSuffixes[i];
if (filename.endsWith(suffix)) {
result = false;
}
}
if (this.verbose) {
console.log("includeThisFile: " + filename + " --> " + result);
}
return result;
}
/**
* Return the name of the platform where this node.js process is running.
* Possible values are 'aix', 'darwin', 'freebsd', 'linux', 'openbsd', 'sunos', and 'win32'.
*/
platform() {
return os_1.default.platform();
}
/**
* Return true if the current platform is Windows, else return false.
*/
isWindows() {
let p = os_1.default.platform().toLowerCase();
if (this.isMac()) {
return false; // 'darwin' contains 'win'!
}
return p.includes('win');
}
/**
* Return true if the current platform is Apple macOS, else return false.
*/
isMac() {
let p = os_1.default.platform().toLowerCase();
return p.includes('darwin');
}
/**
* Return true if the current platform is Linux, else return false.
*/
isLinux() {
let p = os_1.default.platform().toLowerCase();
return p.includes('linux');
}
/**
* Return true if given command line arg (i.e. --verbose) is present, else return false.
*/
cliArgPresent(flag) {
for (let i = 0; i < process.argv.length; i++) {
if (process.argv[i] === flag) {
return true;
}
}
return false;
}
defaultEnvVarPatterns() {
return this.defaultConfig()['env_var_patterns'];
}
defaultExcludeFilePatterns() {
return this.defaultConfig()['exclude_file_patterns'];
}
defaultExcludeFileSuffixes() {
return this.defaultConfig()['exclude_file_suffixes'];
}
/**
* This method returns the default configuration object.
* It is used by default if you don't have a ".evsecrets.json" file
* in the current directory.
*
* This object is also written to your filesystem when the 'init'
* function is executed.
*/
defaultConfig() {
return {
"description": "Configuration file for the evsecrets NPM library CLI program.",
"env_var_patterns": [
"_KEY",
"_URI",
"_URL",
"CONN_STR",
"CONNECTION_STR",
"CONNECTION_STRING"
],
"exclude_file_patterns": [
"__MACOSX/",
"__pycache__/",
".code-workspace",
".git/",
".git/",
".gradle/",
".idea/",
".vscode/",
"bin/",
"build/",
"htmlcov/",
"man/",
"node_modules/",
"obj/",
"opt/",
"tmp/",
"venv/"
],
"exclude_file_suffixes": [
".acc",
".avi",
".bmp",
".class",
".dll",
".doc",
".docx",
".DS_Store",
".exe",
".gif",
".jar",
".jpeg",
".jpg",
".mov",
".mp3",
".mp4",
".pdf",
".png",
".ppt",
".pptx",
".pyc",
".so",
".tar",
".tgz",
".tiff",
".wav",
".xls",
".xlsx",
".vscode",
".zip"
]
};
}
}
exports.EnvScanner = EnvScanner;
EnvScanner.CONFIG_FILE = ".evsecrets.json";
EnvScanner.VERSION = "1.1.0";
// The following line is intentional, it contains the public value of the
// Cosmos DB local emulator, in my system as env var AZURE_COSMOSDB_EMULATOR_KEY.
// This secret value will be identified with a 'scan' function.
// Secret value => C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
//# sourceMappingURL=EnvScanner.js.map