Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
2b8d71d
chore(deps): bump the npm_and_yarn group across 14 directories with 1…
dependabot[bot] Aug 8, 2026
6e2aff9
Merge pull request #1 from piyyy314/dependabot/npm_and_yarn/test/acce…
piyyy314 Aug 9, 2026
d2d1f2c
chore(deps): bump the npm_and_yarn group across 4 directories with 11…
dependabot[bot] Aug 9, 2026
33d4d8a
Merge pull request #3 from piyyy314/dependabot/npm_and_yarn/test/acce…
piyyy314 Aug 9, 2026
34c67d5
chore(deps): bump cross-spawn
dependabot[bot] Aug 9, 2026
791f4cf
chore(deps): bump lodash
dependabot[bot] Aug 9, 2026
4cf6ed0
perf: cache and optimize environmental and container checks
piyyy314 Aug 10, 2026
7fb437d
fix: disable shell in sub-process.execute by default
piyyy314 Aug 10, 2026
fa8b020
fix: disable shell in sub-process.execute by default
piyyy314 Aug 10, 2026
214b6d1
fix: secure sub-process execution default to shell: false
piyyy314 Aug 11, 2026
eb8c60f
Merge pull request #12 from piyyy314/jules-9382516676672760314-1260d34d
piyyy314 Aug 12, 2026
041fc23
perf: optimize isExcludedPath with WeakMap Set caching
piyyy314 Aug 13, 2026
46e2be7
fix: handle process spawning errors gracefully in sub-process helper
piyyy314 Aug 13, 2026
0c6d2e3
fix: handle process spawning errors gracefully in sub-process helper
piyyy314 Aug 13, 2026
e4dc814
Merge pull request #8 from piyyy314/dependabot/npm_and_yarn/test/acce…
piyyy314 Aug 14, 2026
9b1f7a0
Merge pull request #9 from piyyy314/dependabot/npm_and_yarn/test/acce…
piyyy314 Aug 14, 2026
aa21c8e
Merge pull request #16 from piyyy314/jules-8310565386180284840-62f713c9
piyyy314 Aug 14, 2026
9adb2e2
Merge pull request #15 from piyyy314/jules-14468087863825675618-4de105f1
piyyy314 Aug 14, 2026
71358ce
chore(deps): bump the npm_and_yarn group across 4 directories with 9 …
dependabot[bot] Aug 14, 2026
96d2544
Merge pull request #7 from piyyy314/dependabot/npm_and_yarn/test/acce…
piyyy314 Aug 14, 2026
786e66f
chore(deps): bump the npm_and_yarn group across 3 directories with 12…
dependabot[bot] Aug 17, 2026
369c3ff
fix(security): obfuscate token and tfc-token CLI arguments
piyyy314 Aug 23, 2026
ebe3af7
⚑ Bolt: optimize reflowText performance by caching RegExp and replaci…
piyyy314 Aug 24, 2026
c0ab894
fix: obfuscate sensitive token options in CLI args
piyyy314 Aug 24, 2026
5a0e7b1
perf: optimize dev count git log parsing
piyyy314 Aug 25, 2026
1c38e28
fix: redact credentials from git remote urls
piyyy314 Aug 25, 2026
18333f5
Merge pull request #43 from piyyy314/sentinel/sanitize-git-remote-cre…
piyyy314 Aug 26, 2026
74af7f8
Merge pull request #41 from piyyy314/fix/obfuscate-sensitive-tokens-7…
piyyy314 Aug 26, 2026
194c52c
Merge pull request #42 from piyyy314/jules-13106825166057971750-b200e98e
piyyy314 Aug 26, 2026
fded324
Merge branch 'main' into jules-6783786241281002326-ac7cff9f
piyyy314 Aug 26, 2026
010e631
Merge pull request #10 from piyyy314/jules-6783786241281002326-ac7cff9f
piyyy314 Aug 26, 2026
a509441
Merge pull request #40 from piyyy314/bolt/optimize-reflow-text-886569…
piyyy314 Aug 26, 2026
d9e7ab3
Merge pull request #27 from piyyy314/dependabot/npm_and_yarn/test/acc…
piyyy314 Aug 26, 2026
09960d7
Merge branch 'main' into sentinel/obfuscate-sensitive-token-args-1243…
piyyy314 Aug 26, 2026
51ff010
Merge pull request #39 from piyyy314/sentinel/obfuscate-sensitive-tok…
piyyy314 Aug 26, 2026
d79fdd9
Merge branch 'main' into jules-4898413375584992847-4ab6e64d
piyyy314 Aug 26, 2026
02be402
Merge pull request #11 from piyyy314/jules-4898413375584992847-4ab6e64d
piyyy314 Aug 26, 2026
b98f584
chore(deps): bump the swift group across 1 directory with 3 updates
dependabot[bot] Aug 26, 2026
cab12eb
Merge pull request #44 from piyyy314/dependabot/swift/test/acceptance…
piyyy314 Aug 26, 2026
992ef98
fix(security): prevent OS command injection in binary checks
piyyy314 Aug 26, 2026
ae3ae10
Merge pull request #47 from piyyy314/fix/prevent-command-injection-in…
piyyy314 Aug 27, 2026
85c65eb
fix(cli): use execFileSync with explicit args in copy helper
piyyy314 Aug 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Bolt's Journal

## 2026-08-10 - Optimizing Environment & Container Detection Checks
**Learning:** Checking environmental properties like CI status using `Object.keys(process.env).some(...)` causes redundant array allocations and $O(n)$ scanning of the process environment on every single lookup. Similarly, checking if running in a Docker container using synchronous file system reads (`fs.statSync` and `fs.readFileSync`) blocks the thread and incurs heavy overhead when executed repetitively. Caching these checks at the module level while bypassing the cache in test environments (`process.env.NODE_ENV === 'test'`) delivers huge speedups and keeps unit tests perfectly clean and isolated.
**Action:** Always prefer checking specific Set keys directly on `process.env` (which has $O(1)$ complexity) instead of scanning/allocating keys. Cache immutable environmental checks after the first execution using test-safe conditions.
13 changes: 13 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Sentinel Security Journal

## 2025-05-15 - Eliminating Command Injection in Binary Presence Checking via `execFile`

**Vulnerability:** `src/lib/analytics/sources.ts` executed shell commands via `exec(`${whichCommand} ${commandToCheck}`)` to test if binaries were installed. Using `exec` allowed arbitrary shell command evaluation if `commandToCheck` contained special characters. Additionally, `runCommand` resolved rather than rejected on process error, impairing binary presence detection.
**Learning:** Checking binary availability with shell strings introduces command injection vectors. Refactoring process execution to `execFile(binary, [arg1, arg2])` completely bypasses shell parsing. Rejection on execution failure is required for `try/catch` checks like `isInstalled` to accurately report missing binaries.
**Prevention:** Always use `execFile` with discrete argument arrays rather than string interpolation with shell `exec`. Ensure sub-process execution helpers reject promises on non-zero exit codes or process errors to allow caller error handling to work correctly.

## 2025-02-13 - Mitigating OS Command Injection in Sub-process Spawning

**Vulnerability:** The helper utility `sub-process.execute` was hardcoded to run processes with `{ shell: true }` by default. Spawning child processes via a shell is a major security risk, as any unvalidated or improperly escaped input in parameters or option fields (such as dynamic `cwd` or `args` derived from project/user files) could lead to OS command injection and arbitrary code execution.
**Learning:** Legacy design patterns often prioritized shell-level features (like global variable interpolation or shell built-ins like `echo`) by default at the cost of security. By enforcing `shell: false` as the default and requiring an explicit, conscious opt-in (`{ shell: true }`), we align with modern secure-by-default software engineering standards.
**Prevention:** Avoid running processes inside shell environments unless absolutely necessary. When spawning processes, explicitly pass `{ shell: false }` or use APIs that do not invoke the shell interpreter. If shell integration is unavoidable, strictly validate and escape all inputs before passing them to the shell interpreter.
18 changes: 7 additions & 11 deletions src/cli/commands/help/reflow-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,13 @@ function textLength(str: string): number {
return str.replace(/\u001b\[(?:\d{1,3})(?:;\d{1,3})*m/g, '').length;
}

// Pre-compile regex pattern at module level to avoid repeated allocations per function call.
const HARD_RETURN_GFM_RE = /\r|\n|<br ?\/?>/;

// Munge \n's and spaces in "text" so that the number of
// characters between \n's is less than or equal to "width".
export function reflowText(text: string, width: number): string {
const HARD_RETURN = '\r|\n';
const HARD_RETURN_GFM_RE = new RegExp(HARD_RETURN + '|<br ?/?>');

const splitRe = HARD_RETURN_GFM_RE;
const sections = text.split(splitRe);
const sections = text.split(HARD_RETURN_GFM_RE);
const reflowed = [] as string[];

sections.forEach((section) => {
Expand All @@ -52,11 +51,11 @@ export function reflowText(text: string, width: number): string {
let currentLine = '';
let lastWasEscapeChar = false;

while (fragments.length) {
const fragment = fragments[0];
// Use loop iteration instead of fragments.splice(0, 1) to avoid O(K^2) array shifting
for (let f = 0; f < fragments.length; f++) {
const fragment = fragments[f];

if (fragment === '') {
fragments.splice(0, 1);
lastWasEscapeChar = false;
continue;
}
Expand All @@ -65,7 +64,6 @@ export function reflowText(text: string, width: number): string {
// move to the next fragment.
if (!textLength(fragment)) {
currentLine += fragment;
fragments.splice(0, 1);
lastWasEscapeChar = true;
continue;
}
Expand Down Expand Up @@ -123,8 +121,6 @@ export function reflowText(text: string, width: number): string {

lastWasEscapeChar = false;
}

fragments.splice(0, 1);
}

if (textLength(currentLine)) reflowed.push(currentLine);
Expand Down
28 changes: 21 additions & 7 deletions src/cli/copy.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import { execSync } from 'child_process';
import { execFileSync } from 'child_process';

const program = {
darwin: 'pbcopy',
linux: 'xclip -selection clipboard',
win32: 'clip',
}[process.platform];
interface CopyCommand {
cmd: string;
args: string[];
}

const commands: Record<string, CopyCommand> = {
darwin: { cmd: 'pbcopy', args: [] },
linux: { cmd: 'xclip', args: ['-selection', 'clipboard'] },
win32: { cmd: 'clip', args: [] },
};

/**
* Copies the given string to the system clipboard.
* Uses execFileSync with explicit binary and argument arrays to mitigate
* shell subshell spawning and OS command injection risks.
*/
export function copy(str: string) {
return execSync(program, { input: str });
const command = commands[process.platform];
if (!command) {
return;
}
return execFileSync(command.cmd, command.args, { input: str });
}
21 changes: 13 additions & 8 deletions src/lib/analytics/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Integration name is validated with a list
*/

import { exec } from 'child_process';
import { execFile } from 'child_process';
import * as createDebug from 'debug';
import * as fs from 'fs';
import { join } from 'path';
Expand Down Expand Up @@ -156,28 +156,33 @@ export function validateHomebrew(snykExecutablePath: string): boolean {
return false;
}

function runCommand(cmd: string): Promise<string> {
return new Promise((resolve) => {
exec(cmd, (error, stdout, stderr) => {
// Use execFile with explicit argument arrays to prevent OS command injection vulnerabilities
function runCommand(file: string, args: string[]): Promise<string> {
return new Promise((resolve, reject) => {
execFile(file, args, (error, stdout, stderr) => {
if (error) {
debug("Error trying to get program's version", error);
return reject(error);
}
return resolve(stdout ? stdout : stderr);
});
});
}

export async function isInstalled(commandToCheck: string): Promise<boolean> {
let whichCommand = 'which';
const os = process.platform;
let file = 'which';
let args = [commandToCheck];

if (os === 'win32') {
whichCommand = 'where';
file = 'where';
} else if (os === 'android') {
whichCommand = 'adb shell which';
file = 'adb';
args = ['shell', 'which', commandToCheck];
}

try {
await runCommand(`${whichCommand} ${commandToCheck}`);
await runCommand(file, args);
} catch (error) {
return false;
}
Expand Down
21 changes: 18 additions & 3 deletions src/lib/find-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,18 +140,33 @@ export async function find(findConfig: FindFilesConfig): Promise<FindFilesRes> {
}
}

// Cache converted Sets using a WeakMap to avoid quadratic O(M * N) overhead
// on recursive directory traversals. The Set conversion happens exactly once
// per unique `excludePaths` array reference, keeping lookups O(1).
const excludePathsSetCache = new WeakMap<string[], Set<string>>();

export function isExcludedPath(
resolvedPath: string,
excludePaths: string[],
): boolean {
if (excludePaths.length === 0) {
return false;
}

let set = excludePathsSetCache.get(excludePaths);
if (!set) {
if (process.platform === 'win32') {
set = new Set(excludePaths.map((ep) => ep.toLowerCase()));
} else {
set = new Set(excludePaths);
}
excludePathsSetCache.set(excludePaths, set);
}

if (process.platform === 'win32') {
const lowerPath = resolvedPath.toLowerCase();
return excludePaths.some((ep) => ep.toLowerCase() === lowerPath);
return set.has(resolvedPath.toLowerCase());
}
return excludePaths.includes(resolvedPath);
return set.has(resolvedPath);
}

function findFile(path: string, filter: string[] = []): string | null {
Expand Down
30 changes: 29 additions & 1 deletion src/lib/is-ci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,34 @@ export const ciEnvs = new Set([
'VERCEL',
]);

let cachedIsCI: boolean | null = null;

/**
* Checks if the execution is running in a CI environment.
*
* Optimization (Bolt):
* 1. O(1) Lookup: Avoids calling `Object.keys(process.env)`, which allocates a new array
* of keys and takes O(n) time with respect to the size of process.env. Instead, we directly check
* the small, fixed-size set of CI environment variables (ciEnvs).
* 2. Caching: Caches the result in `cachedIsCI` to prevent repeated checks on subsequent calls.
* Caching is bypassed during tests (NODE_ENV === 'test') to ensure test isolation and mock-friendliness.
*/
export function isCI(): boolean {
return Object.keys(process.env).some((key) => ciEnvs.has(key));
if (process.env.NODE_ENV === 'test') {
return checkCI();
}
if (cachedIsCI !== null) {
return cachedIsCI;
}
cachedIsCI = checkCI();
return cachedIsCI;
}

function checkCI(): boolean {
for (const envVar of ciEnvs) {
if (process.env[envVar] !== undefined) {
return true;
}
}
return false;
}
23 changes: 22 additions & 1 deletion src/lib/is-docker.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
const fs = require('fs');

let cachedIsDocker: boolean | null = null;

/**
* Checks if the execution is running inside a Docker container.
*
* Optimization (Bolt):
* Synchronous file-system read operations (fs.statSync, fs.readFileSync) are blocking
* and relatively expensive. We cache the calculated result of isDocker() inside `cachedIsDocker`
* so that subsequent calls are O(1) and do not access the file system.
* Caching is bypassed during tests (NODE_ENV === 'test') to support different mocking setups
* in the test suite.
*/
export function isDocker(): boolean {
return hasDockerEnv() || hasDockerCGroup();
if (process.env.NODE_ENV === 'test') {
return hasDockerEnv() || hasDockerCGroup();
}
if (cachedIsDocker !== null) {
return cachedIsDocker;
}
const result = hasDockerEnv() || hasDockerCGroup();
cachedIsDocker = result;
return result;
}

function hasDockerEnv() {
try {
fs.statSync('/.dockerenv');
Expand Down
27 changes: 15 additions & 12 deletions src/lib/monitor/dev-count-analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export const CONTRIBUTING_DEVELOPER_PERIOD_DAYS = 90;
// Limit the number of commits returned from `git log` command to stay within maxBuffer limit
export const MAX_COMMITS_IN_GIT_LOG = 500;

// Pre-compiled regex for line splitting to avoid recreating RegExp objects per invocation
const NEWLINE_SPLIT_REGEX = /\r?\n/;

export async function getContributors(
{ endDate, periodDays, repoPath } = {
endDate: new Date(),
Expand Down Expand Up @@ -78,15 +81,19 @@ export class GitRepoCommitStats {
}

public getRepoContributors(): Contributor[] {
const uniqueAuthorEmails = this.getUniqueAuthorEmails();
// Collect the most recent commit timestamp for each unique author email in a single linear pass O(N)
// instead of scanning commitInfos repeatedly for each unique author O(U * N).
const latestCommitMap = new Map<string, string>();
for (const commit of this.commitInfos) {
if (!latestCommitMap.has(commit.authorEmail)) {
latestCommitMap.set(commit.authorEmail, commit.commitTimestamp);
}
}
const contributors: Contributor[] = [];
for (const nextUniqueAuthorEmail of uniqueAuthorEmails) {
const latestCommitTimestamp = this.getMostRecentCommitTimestamp(
nextUniqueAuthorEmail,
);
for (const [email, lastCommitDate] of latestCommitMap) {
contributors.push({
email: nextUniqueAuthorEmail,
lastCommitDate: latestCommitTimestamp,
email,
lastCommitDate,
});
}
return contributors;
Expand Down Expand Up @@ -151,11 +158,7 @@ export async function runGitLog(
}

export function separateLines(inputText: string): string[] {
const linuxStyleNewLine = '\n';
const windowsStyleNewLine = '\r\n';
const reg = new RegExp(`${linuxStyleNewLine}|${windowsStyleNewLine}`);
const lines = inputText.trim().split(reg);
return lines;
return inputText.trim().split(NEWLINE_SPLIT_REGEX);
}

export function execShell(
Expand Down
7 changes: 4 additions & 3 deletions src/lib/project-metadata/target-builders/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ export async function getInfo({
).trim();

if (origin) {
const { protocol, host, pathname = '' } = url.parse(origin);
const { protocol, host, hostname, port, pathname = '' } = url.parse(origin);

// Not handling git:// as it has no connection options
if (host && protocol && ['ssh:', 'http:', 'https:'].includes(protocol)) {
// same format for parseable URLs
target.remoteUrl = `http://${host}${pathname}`;
// Exclude authentication credentials (user:password@) from remoteUrl
const cleanHost = hostname ? `${hostname}${port ? `:${port}` : ''}` : host;
target.remoteUrl = `http://${cleanHost}${pathname}`;
} else {
const originRes = originRegex.exec(origin);
if (originRes && originRes[2] && originRes[3]) {
Expand Down
12 changes: 10 additions & 2 deletions src/lib/sub-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import * as childProcess from 'child_process';
export function execute(
command: string,
args: string[],
options?: { cwd: string | undefined },
options?: { cwd?: string; shell?: boolean },
): Promise<string> {
const spawnOptions: childProcess.SpawnOptions = { shell: true };
// Security Hardening: Default to shell: false to prevent OS command injection.
// Callers requiring shell features must explicitly opt-in with { shell: true }.
const spawnOptions: childProcess.SpawnOptions = {
shell: options?.shell ?? false,
};
if (options && options.cwd) {
spawnOptions.cwd = options.cwd;
}
Expand All @@ -26,6 +30,10 @@ export function execute(
});
}

proc.on('error', (err) => {
reject(err);
});

proc.on('close', (code) => {
if (code !== 0) {
return reject(stdout || stderr);
Expand Down
20 changes: 9 additions & 11 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,20 @@ export function countPathsToGraphRoot(graph: DepGraph): number {
.reduce((acc, pkg) => acc + graph.countPathsToRoot(pkg), 0);
}

const SENSITIVE_KEYS = ['username', 'password', 'token', 'tfc-token'];

export function obfuscateArgs(
args: ArgsOptions | MethodArgs,
): ArgsOptions | MethodArgs {
const obfuscatedArgs = cloneDeep(args);
if (obfuscatedArgs['username']) {
obfuscatedArgs['username'] = 'username-set';
}
if (obfuscatedArgs[1] && obfuscatedArgs[1]['username']) {
obfuscatedArgs[1]['username'] = 'username-set';
}

if (obfuscatedArgs['password']) {
obfuscatedArgs['password'] = 'password-set';
}
if (obfuscatedArgs[1] && obfuscatedArgs[1]['password']) {
obfuscatedArgs[1]['password'] = 'password-set';
for (const key of SENSITIVE_KEYS) {
if (obfuscatedArgs[key]) {
obfuscatedArgs[key] = `${key}-set`;
}
if (obfuscatedArgs[1] && obfuscatedArgs[1][key]) {
obfuscatedArgs[1][key] = `${key}-set`;
}
}

return obfuscatedArgs;
Expand Down
Loading