Skip to content

Fix security code scanning alerts#9

Merged
Fcmam5 merged 11 commits into
masterfrom
fix/security-code-scanning-alerts
Apr 5, 2026
Merged

Fix security code scanning alerts#9
Fcmam5 merged 11 commits into
masterfrom
fix/security-code-scanning-alerts

Conversation

@Fcmam5
Copy link
Copy Markdown
Owner

@Fcmam5 Fcmam5 commented Apr 5, 2026

Fix

appeared in https://github.com/Fcmam5/skilleton/actions/runs/24004017072

Summary by CodeRabbit

  • Bug Fixes

    • Hardened git-related operations to reject unsafe inputs, validate commits, and prevent path traversal during repository export.
  • Documentation

    • Updated security support policy and added Automated Alert Triage guidance with SLAs, tracking, and verification guidance.
  • Refactor

    • Improved CLI argument/flag parsing and introduced a filesystem abstraction for safer file handling.
  • Tests

    • Added comprehensive tests and a reusable in-memory filesystem helper; tightened coverage collection.
  • Chores

    • Added security lint rules and updated development toolchain versions.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 5, 2026

📝 Walkthrough

Walkthrough

Adds security hardening and abstractions: FileSystem interface extension (mkdtemp), input validation and path-safety in the git adapter with an injectable FileSystem, ESLint security rules and restricted imports (with an override), CLI parsing/dispatch refactor, SECURITY.md triage guidance, new test helpers and tests for git behavior, and minor utility refactors.

Changes

Cohort / File(s) Summary
Security Documentation
SECURITY.md
Replace generic supported-version line with explicit >=0.3.1, deprecate <0.3.1, add "Automated Alert Triage (GitHub)" section with severity SLAs (7/14/30/90 days) and required alert tracking fields.
Linting & DevDeps
eslint.config.cjs, package.json
Add eslint-plugin-security, merge recommended security rules, add no-restricted-imports banning direct fs imports with an override for src/core/filesystem.ts; bump several devDependencies.
CLI parsing & dispatch
bin/skilleton.ts
Introduce getCommandHandler(command) switch for command lookup and refactor parseArgs() to use a token cursor and Map-backed flag accumulation.
FileSystem API & impl
src/core/types.ts, src/core/filesystem.ts
Add mkdtemp(prefix: string): Promise<string> to FileSystem interface and implement it on NodeFileSystem.
Git adapter hardening
src/adapters/git.ts
Add COMMIT_SHA_PATTERN, assertSafeGitArg, resolveSafeSubPath; inject FileSystem into ExecaGitClient (constructor signature changed); use safe path resolution, add -- to git clone, and replace fs.promises with this.fs.
Lockfile & repo utilities
src/core/lock.ts, src/core/repos.ts
Refactor serialization/prune to use Object.fromEntries/Map lookups; change trailing-slash trimming in normalizeRepoUrl to a loop-based approach.
Describe command
src/commands/describe.ts
Replace optional-chaining lookup with getLockedSkillByName() helper that builds a Map for locked-skill lookup.
Tests & test helpers
tests/helpers/mocked-filesystem.ts, tests/adapters.git.test.ts, tests/describe.test.ts, tests/install.test.ts, jest.config.cjs
Add MockedFileSystem test helper implementing FileSystem (including mkdtemp), add tests for ExecaGitClient validation/worktree behavior, migrate tests to use the helper, and add collectCoverageFrom: ['src/**/*.ts'] to Jest config.

Sequence Diagram(s)

mermaid
sequenceDiagram
autonumber
participant CLI as Client/CLI
participant Git as Git Adapter (Execa)
participant FS as FileSystem
participant Temp as TempWorktree
CLI->>Git: ensureRepo(repoCachePath, cloneSource)
Note right of Git: assertSafeGitArg(repoCachePath/cloneSource)
Git->>FS: pathExists(repoCachePath)?
alt cache missing
Git->>Git: git clone ... --
else cache exists
Git->>Git: git -C fetch --tags --prune --force
end
CLI->>Git: exportPath(repoPath, commit, subPath, destination)
Git->>Git: validate commit via COMMIT_SHA_PATTERN
Git->>FS: mkdtemp(prefix) -> worktree
Git->>Git: git worktree add --force --detach
Git->>FS: copy(/, destination)
Git->>Git: git worktree remove --force
Git->>FS: remove()

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through paths both safe and neat,
I barred the ../ and sealed each street,
Mkdtemp made a cozy burrow bright,
Lint guards kept watch through day and night,
Tests tidy tunnels — snug and right.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix security code scanning alerts' is directly related to the primary objective of addressing security code scanning alerts through implementation of security hardening measures.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/security-code-scanning-alerts

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/adapters/git.ts (1)

101-115: ⚠️ Potential issue | 🟠 Major

Reject invalid subPath before allocating the worktree.

Lines 101-115 only run the traversal check after mkdtemp() and the external worktree setup. src/core/install.ts passes skill.path straight into exportPath(), so a manifest value like ../escape still triggers side effects before we reject it. Move the subPath validation ahead of any filesystem/process work, then flip tests/adapters.git.test.ts to assert no mkdtemp() or worktree calls on invalid paths.

Suggested fix
+function assertSafeSubPath(subPath?: string): void {
+  if (!subPath || subPath === '.') {
+    return;
+  }
+
+  if (path.isAbsolute(subPath)) {
+    throw new SkillInstallError(`Invalid skill path ${subPath}`);
+  }
+
+  const normalized = path.normalize(subPath);
+  if (normalized === '..' || normalized.startsWith(`..${path.sep}`)) {
+    throw new SkillInstallError(`Invalid skill path ${subPath}`);
+  }
+}
+
 function resolveSafeSubPath(baseDir: string, subPath?: string): string {
-  if (!subPath || subPath === '.') {
-    return baseDir;
-  }
-
-  if (path.isAbsolute(subPath)) {
-    throw new SkillInstallError(`Invalid skill path ${subPath}`);
-  }
+  assertSafeSubPath(subPath);
+
+  if (!subPath || subPath === '.') {
+    return baseDir;
+  }
 
   const normalized = path.resolve(baseDir, subPath);
   const relativeToBase = path.relative(baseDir, normalized);
-  if (relativeToBase.startsWith('..') || path.isAbsolute(relativeToBase)) {
+  if (relativeToBase === '..' || relativeToBase.startsWith(`..${path.sep}`) || path.isAbsolute(relativeToBase)) {
     throw new SkillInstallError(`Invalid skill path ${subPath}`);
   }
@@
   async exportPath(repoPath: string, commit: string, destination: string, subPath?: string): Promise<void> {
     assertSafeGitArg(repoPath, 'repository path');
     assertSafeGitArg(destination, 'destination path');
+    assertSafeSubPath(subPath);
     if (!COMMIT_SHA_PATTERN.test(commit)) {
       throw new SkillInstallError(`Invalid commit SHA: ${commit}`);
     }
 
     const worktreeDir = await this.fs.mkdtemp(path.join(os.tmpdir(), 'skilleton-wt-'));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/adapters/git.ts` around lines 101 - 115, Validate the requested subPath
(reject path traversal like "../...") before creating the temp worktree: call
your safe path check (e.g., resolveSafeSubPath or a dedicated validateSubPath
function using repoPath and subPath) and throw the SkillInstallError if invalid
prior to calling this.fs.mkdtemp or running execa('git', ['worktree', ...]) so
no temp dir or git worktree is allocated for bad inputs; then update
tests/adapters.git.test.ts to expect no mkdtemp or git worktree calls when
passing an invalid manifest path.
🧹 Nitpick comments (2)
eslint.config.cjs (1)

27-41: Consider also restricting the bare fs import.

The rule blocks node:fs and node:fs/promises but not the legacy fs import (without the node: prefix). While modern code typically uses the prefixed form, adding fs and fs/promises would ensure complete coverage.

♻️ Proposed addition to cover legacy imports
           'no-restricted-imports': [
             'error',
             {
               paths: [
                 {
                   name: 'node:fs',
                   message: FILESYSTEM_IMPORT_MESSAGE,
                 },
                 {
                   name: 'node:fs/promises',
                   message: FILESYSTEM_IMPORT_MESSAGE,
                 },
+                {
+                  name: 'fs',
+                  message: FILESYSTEM_IMPORT_MESSAGE,
+                },
+                {
+                  name: 'fs/promises',
+                  message: FILESYSTEM_IMPORT_MESSAGE,
+                },
               ],
             },
           ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eslint.config.cjs` around lines 27 - 41, The no-restricted-imports rule
currently blocks 'node:fs' and 'node:fs/promises' but not their legacy
counterparts; update the 'no-restricted-imports' configuration (the rule named
no-restricted-imports) to also include 'fs' and 'fs/promises' entries using the
same FILESYSTEM_IMPORT_MESSAGE so legacy imports are covered alongside 'node:fs'
and 'node:fs/promises'.
src/core/filesystem.ts (1)

33-35: Add JSDoc comment for consistency with other methods.

Other methods in NodeFileSystem have JSDoc comments documenting their purpose and parameters. Consider adding one here for consistency.

📝 Proposed JSDoc addition
+  /**
+   * Creates a unique temporary directory.
+   * `@param` prefix Directory path prefix (must include trailing path separator if targeting a specific directory).
+   */
   async mkdtemp(prefix: string): Promise<string> {
     return fs.mkdtemp(prefix);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/filesystem.ts` around lines 33 - 35, Add a JSDoc block for the
NodeFileSystem.mkdtemp method to match other methods: document the purpose
(create a new temporary directory), describe the prefix parameter (string used
to name the temp dir), and the return type (Promise<string> resolving to the
created directory path). Place the comment immediately above the async
mkdtemp(prefix: string): Promise<string> method so it follows the same JSDoc
style and tags used by other NodeFileSystem methods.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/describe.test.ts`:
- Around line 277-281: The in-memory mkdtemp implementation always returns the
same path for a given prefix; update mkdtemp to produce a unique directory per
call (matching FileSystem.mkdtemp semantics) by appending a unique suffix (e.g.,
a random string or incrementing counter) to `${prefix}tmp` before calling
this.normalize, then add that unique path to this.directories and return it;
modify the mkdtemp function (references: mkdtemp, this.normalize,
this.directories) so repeated calls with the same prefix yield different
directory paths.

---

Outside diff comments:
In `@src/adapters/git.ts`:
- Around line 101-115: Validate the requested subPath (reject path traversal
like "../...") before creating the temp worktree: call your safe path check
(e.g., resolveSafeSubPath or a dedicated validateSubPath function using repoPath
and subPath) and throw the SkillInstallError if invalid prior to calling
this.fs.mkdtemp or running execa('git', ['worktree', ...]) so no temp dir or git
worktree is allocated for bad inputs; then update tests/adapters.git.test.ts to
expect no mkdtemp or git worktree calls when passing an invalid manifest path.

---

Nitpick comments:
In `@eslint.config.cjs`:
- Around line 27-41: The no-restricted-imports rule currently blocks 'node:fs'
and 'node:fs/promises' but not their legacy counterparts; update the
'no-restricted-imports' configuration (the rule named no-restricted-imports) to
also include 'fs' and 'fs/promises' entries using the same
FILESYSTEM_IMPORT_MESSAGE so legacy imports are covered alongside 'node:fs' and
'node:fs/promises'.

In `@src/core/filesystem.ts`:
- Around line 33-35: Add a JSDoc block for the NodeFileSystem.mkdtemp method to
match other methods: document the purpose (create a new temporary directory),
describe the prefix parameter (string used to name the temp dir), and the return
type (Promise<string> resolving to the created directory path). Place the
comment immediately above the async mkdtemp(prefix: string): Promise<string>
method so it follows the same JSDoc style and tags used by other NodeFileSystem
methods.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0d306203-5979-48dc-a1a8-b18b6070d2fb

📥 Commits

Reviewing files that changed from the base of the PR and between 1b4c861 and 6613160.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (13)
  • SECURITY.md
  • bin/skilleton.ts
  • eslint.config.cjs
  • package.json
  • src/adapters/git.ts
  • src/commands/describe.ts
  • src/core/filesystem.ts
  • src/core/lock.ts
  • src/core/repos.ts
  • src/core/types.ts
  • tests/adapters.git.test.ts
  • tests/describe.test.ts
  • tests/install.test.ts

Comment thread tests/describe.test.ts Outdated
Fcmam5 added 3 commits April 5, 2026 23:55
…age config

- Move InMemoryFileSystem from describe.test.ts to tests/helpers/mocked-filesystem.ts
- Rename InMemoryFileSystem to MockedFileSystem with jest.fn() methods
- Add createMockedFileSystem() factory function
- Update all tests to use shared MockedFileSystem helper
- Implement missing FileSystem methods (readJson, writeJson, remove, copy, symlink)
- Configure Jest to collect coverage from src/**/*.ts files only
- Add test for fetching existing repositories instead of cloning
- Add test for rejecting absolute subPath values before worktree allocation
- Add test for successful path export with worktree cleanup
- Add test for wrapping missing source path errors while ensuring worktree cleanup
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/helpers/mocked-filesystem.ts (1)

117-121: Align symlink mock with production replacement semantics.

Line 117–121 does not remove an existing destination before creating the mock symlink entry. Matching production behavior will make symlink-related tests more faithful.

Proposed refactor
   symlink = jest.fn(async (_target: string, dest: string): Promise<void> => {
     const normalizedDest = this.normalize(dest);
+    if (await this.pathExists(normalizedDest)) {
+      await this.remove(normalizedDest);
+    }
     this.directories.add(path.dirname(normalizedDest));
     this.files.set(normalizedDest, '');
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/helpers/mocked-filesystem.ts` around lines 117 - 121, The mock symlink
implementation (symlink) should mirror production semantics by removing any
existing entry at the normalized destination before creating the new symlink;
update the async symlink mock to compute normalizedDest via
this.normalize(dest), then if an existing file or symlink exists at
normalizedDest delete/unset it (e.g., remove from this.files and any metadata)
before adding the new entry and ensuring this.directories includes
path.dirname(normalizedDest); refer to the symlink function and
this.files/this.directories/this.normalize symbols when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/helpers/mocked-filesystem.ts`:
- Around line 13-16: The mocked addDirectory implementation only adds the target
path and not its ancestor directories, diverging from real
NodeFileSystem.ensureDir; update addDirectory (and any similar methods) to
iterate up the directory path produced by normalize and add each ancestor
directory into this.directories so that all parent directories exist (e.g., walk
parents from the normalized path to root and this.directories.add(...) for
each), ensuring test filesystem state mirrors recursive directory creation.
- Around line 148-150: The readDir implementation incorrectly only throws when a
directory is missing and entries.size === 0, letting calls succeed for
non-existent directories; update the check in readDir to throw whenever
this.directories does not contain the normalized path (i.e., remove the
entries.size condition) so any missing directory (normalized/target) always
raises an Error; reference variables/functions: readDir, this.directories,
normalized, target, entries.

---

Nitpick comments:
In `@tests/helpers/mocked-filesystem.ts`:
- Around line 117-121: The mock symlink implementation (symlink) should mirror
production semantics by removing any existing entry at the normalized
destination before creating the new symlink; update the async symlink mock to
compute normalizedDest via this.normalize(dest), then if an existing file or
symlink exists at normalizedDest delete/unset it (e.g., remove from this.files
and any metadata) before adding the new entry and ensuring this.directories
includes path.dirname(normalizedDest); refer to the symlink function and
this.files/this.directories/this.normalize symbols when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a8625982-ab37-4aa7-904d-ac343b6b18c3

📥 Commits

Reviewing files that changed from the base of the PR and between 6613160 and c5feddb.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • eslint.config.cjs
  • jest.config.cjs
  • package.json
  • src/adapters/git.ts
  • src/core/filesystem.ts
  • tests/adapters.git.test.ts
  • tests/describe.test.ts
  • tests/helpers/mocked-filesystem.ts
  • tests/install.test.ts
✅ Files skipped from review due to trivial changes (2)
  • tests/install.test.ts
  • package.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • eslint.config.cjs
  • tests/describe.test.ts
  • tests/adapters.git.test.ts
  • src/adapters/git.ts

Comment thread tests/helpers/mocked-filesystem.ts
Comment thread tests/helpers/mocked-filesystem.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@SECURITY.md`:
- Around line 7-10: Update SECURITY.md to point to the correct security advisory
URL and make the version recommendation consistent with package.json: verify the
advisory URL (replace the broken
https://github.com/Fcmam5/skilleton/security/advisories/GHSA-5g3j-89fr-r2vp with
the correct advisory link) and update the version text from `0.3.1` to the
actual released version (`0.3.0`) or, if the intent is to require `0.3.1`, bump
the version in package.json accordingly; ensure the table rows and the sentence
about upgrading use the same version string.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d28ebdf4-e1e2-4d87-b993-f516737b6127

📥 Commits

Reviewing files that changed from the base of the PR and between 8b55ac5 and a4ea776.

📒 Files selected for processing (1)
  • SECURITY.md

Comment thread SECURITY.md
Comment on lines +7 to +10
| >=0.3.1 | ✅ |
| <0.3.1 | ❌ (deprecated) |

Skilleton CLI is currently pre-1.0, but we treat every tagged release as security-supported until a newer patch is published.
Versions earlier than `0.3.1` are deprecated due to security fixes (see the [Security Advisory](https://github.com/Fcmam5/skilleton/security/advisories/GHSA-5g3j-89fr-r2vp)). Please upgrade to `0.3.1` or later.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify security advisory URL and version 0.3.1 in package.json

# Check if the security advisory URL is accessible
echo "Checking security advisory URL..."
curl -s -o /dev/null -w "%{http_code}" https://github.com/Fcmam5/skilleton/security/advisories/GHSA-5g3j-89fr-r2vp

# Check if version 0.3.1 exists in package.json
echo -e "\nChecking version in package.json..."
if [ -f package.json ]; then
  cat package.json | jq -r '.version'
else
  echo "package.json not found"
fi

# Search for version references in the codebase
echo -e "\nSearching for version 0.3.1 references..."
rg -n "0\.3\.1" --type=json

Repository: Fcmam5/skilleton

Length of output: 540


Fix broken security advisory link and version mismatch in SECURITY.md.

Lines 7-10 reference a security advisory URL that returns a 404 error (GHSA-5g3j-89fr-r2vp), and the documentation recommends upgrading to version 0.3.1, but the current version in package.json is 0.3.0. Verify that the advisory URL is correct and update the version reference to match the actual released version.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@SECURITY.md` around lines 7 - 10, Update SECURITY.md to point to the correct
security advisory URL and make the version recommendation consistent with
package.json: verify the advisory URL (replace the broken
https://github.com/Fcmam5/skilleton/security/advisories/GHSA-5g3j-89fr-r2vp with
the correct advisory link) and update the version text from `0.3.1` to the
actual released version (`0.3.0`) or, if the intent is to require `0.3.1`, bump
the version in package.json accordingly; ensure the table rows and the sentence
about upgrading use the same version string.

@Fcmam5 Fcmam5 merged commit ddf8e99 into master Apr 5, 2026
4 checks passed
@Fcmam5 Fcmam5 deleted the fix/security-code-scanning-alerts branch April 5, 2026 22:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant