Skip to content

Add workflow option to stats command#247

Open
Toby-Masters-SF wants to merge 12 commits into
mainfrom
241-workflow-test-statistics
Open

Add workflow option to stats command#247
Toby-Masters-SF wants to merge 12 commits into
mainfrom
241-workflow-test-statistics

Conversation

@Toby-Masters-SF

@Toby-Masters-SF Toby-Masters-SF commented Feb 15, 2026

Copy link
Copy Markdown
Contributor

Fixes # (link to the corresponding issue if applicable)

Description

Include a summary of the changes made

Testing Instructions

Steps:

  1. ...
  2. ...
  3. ...

Author Checklist

  • Skip bumping the CLI version

Reviewer Checklist

  • PR has a clear title and description
  • Manually tested the changes that the PR introduces
  • Changes introduced are covered by tests of acceptable quality

Notes

Plan

  • Create a new getWorkflowTemplateSummary(workflowHandle) function
  • Create a new listWorkflowTemplates function which can take different classes?

Testing notes

  • Reconciliations (cross-referenced using AI to interrogate). Reason for difference is multiple yml files in the same tests folder so may need to update the Regex expression: .*${FOLDER}/${templatePattern}/tests/.*_liquid_test.*.y(a)?ml. Resolved by temporarily deleting "extra" tests in test branch
    • Total - 58: CORRECT
    • Externally maanged - 58: CORRECT
    • YAML - at least 1 test - 49: TO CHECK? (AI GIVING 46)
    • YAML - 2+ tests - 45: TO CHECK? (AI GIVING 43)

Quick Notes

  • Plan is to create a series of parallel functions to avoid overcomplicating the existing functions
    • May also wish to consider moving to a separate file in the cli folder?
  • Review function getTemplatesSummary --> Can it be co-opted for the purposes of this workflow? Take the "Reconciliations" section as an example!
    • [TBC]

@Toby-Masters-SF Toby-Masters-SF self-assigned this Feb 15, 2026
@Toby-Masters-SF Toby-Masters-SF linked an issue Feb 15, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Feb 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2485cd91-5a32-4a61-a7f1-8f9ed9991d9b

📥 Commits

Reviewing files that changed from the base of the PR and between e099926 and cad924b.

📒 Files selected for processing (1)
  • lib/utils/fsUtils.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/utils/fsUtils.js

Walkthrough

The PR extends the stats CLI command to support per-workflow statistics reporting. It adds a --workflow option that generates per-workflow template aggregates, YAML activity summaries, and CSV output, alongside utilities to load and process workflow definitions.

Changes

Per-workflow statistics reporting

Layer / File(s) Summary
Workflow file loading utility
lib/utils/fsUtils.js
New getWorkflow(workflowHandle) function loads and parses workflow JSON files from the workflows/ directory, with error logging and process exit on failures. Exported via module.exports.
Per-workflow data aggregation
lib/cli/stats.js
New getWorkflowTemplateSummary(workflow) computes per-workflow reconciliation, account, and export totals and percentages. New getYamlSummary(sinceDate, templatesInWorkflow) scopes YAML file activity counting to templates present in the workflow.
Workflow-scoped YAML file matching and counting
lib/cli/stats.js
yamlFilesActivity updated to accept workflow context and construct a template-based YAML regex pattern. countYamlFiles refactored to accept templatesInWorkflow parameter and incorporate template list into file matching logic.
Workflow statistics output formatting
lib/cli/stats.js
New displayWorkflowOverview prints workflow-scoped summary sections. New createWorkflowRow serializes per-workflow summary fields into semicolon-delimited CSV format. New saveWorkflowOverviewToFile writes workflow-specific CSV rows to ./stats/${workflowHandle}_stats.csv. Module exports updated to include generateWorkflowOverview.
Workflow overview generation orchestration
lib/cli/stats.js
New exported generateWorkflowOverview(sinceDate, workflowHandle) determines workflow handles (single if provided, otherwise reads workflows/*.json), iterates workflows, computes per-workflow template and YAML summaries, displays output, and appends CSV rows.
CLI command definition and routing
bin/cli.js
stats command extended with --workflow [handle] option. Action logic conditionally calls generateWorkflowOverview when --workflow is provided, otherwise falls back to generateOverview.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related issues

  • Workflow test statistics #241 — This PR directly implements the workflow-level stats feature by adding the --workflow CLI option, per-workflow aggregation functions, and workflow file loading utilities requested in that issue.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description uses the template structure but most content areas are placeholder text or incomplete; testing instructions are empty placeholders and the description section contains only template boilerplate. Replace placeholder text with actual descriptions. Fill in the Description section summarizing the changes, complete the Testing Instructions with specific steps, and clarify the implementation details based on the Notes section.
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a workflow option to the stats command.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 241-workflow-test-statistics

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


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

@Toby-Masters-SF Toby-Masters-SF force-pushed the 241-workflow-test-statistics branch 3 times, most recently from 0e6fed1 to 3fb5199 Compare March 18, 2026 14:41
@Toby-Masters-SF Toby-Masters-SF force-pushed the 241-workflow-test-statistics branch from 3fb5199 to b134da4 Compare March 18, 2026 14:47
@Toby-Masters-SF Toby-Masters-SF force-pushed the 241-workflow-test-statistics branch 2 times, most recently from b6c253d to 66ed189 Compare March 31, 2026 08:13
@Toby-Masters-SF Toby-Masters-SF force-pushed the 241-workflow-test-statistics branch from 66ed189 to 51fc334 Compare March 31, 2026 08:24
@michieldegezelle michieldegezelle marked this pull request as ready for review June 11, 2026 07:15
@michieldegezelle michieldegezelle self-requested a review June 11, 2026 07:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
lib/cli/stats.js (2)

370-371: 💤 Low value

Misleading parameter name templatesInWorkflow.

The parameter is named templatesInWorkflow but it actually receives a full workflow object (which is then passed to yamlFilesActivity). The naming is inconsistent with the actual usage. Consider renaming to workflow for clarity.

-async function getYamlSummary(sinceDate, templatesInWorkflow) {
-  const yamlActivity = await yamlFilesActivity(sinceDate, templatesInWorkflow);
+async function getYamlSummary(sinceDate, workflow) {
+  const yamlActivity = await yamlFilesActivity(sinceDate, workflow);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/cli/stats.js` around lines 370 - 371, The parameter name
templatesInWorkflow in function getYamlSummary is misleading because it actually
expects a full workflow object passed into yamlFilesActivity; rename the
parameter to workflow (update the function signature of getYamlSummary and all
internal references) and update any call sites to pass the same workflow
variable name so usage is consistent with the actual object being passed to
yamlFilesActivity.

319-344: 💤 Low value

No defensive checks for missing workflow properties.

If a workflow JSON file is malformed or missing the templates property (or its sub-properties like reconciliations, exports, accounts), the code will throw with an unclear error. Consider adding defensive checks or clearer error messages.

💡 Optional: Add defensive validation
   // Fetch workflow
   // Assume no empty items if present within the workflow.
+  if (!workflow.templates) {
+    consola.error(`Workflow "${workflow.name || 'unknown'}" is missing 'templates' property`);
+    process.exit(1);
+  }
   summary.workflow_name = workflow.name;
 
   // Reconciliations
-  const reconciliationsInWorkflow = workflow.templates.reconciliations;
+  const reconciliationsInWorkflow = workflow.templates.reconciliations || [];
   // ...
-  const exportFilesInWorkflow = workflow.templates.exports;
+  const exportFilesInWorkflow = workflow.templates.exports || [];
   // ...
-  const accountTemplatesInWorkflow = workflow.templates.accounts;
+  const accountTemplatesInWorkflow = workflow.templates.accounts || [];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/cli/stats.js` around lines 319 - 344, The code assumes workflow.templates
and its sub-properties exist (e.g., workflow.templates.reconciliations,
.exports, .accounts), which can throw on malformed workflow JSON; update the
start of this block to defensively validate workflow and its template arrays (or
normalize them) before using them: check that workflow is truthy, that
workflow.templates is an object, and that workflow.templates.reconciliations,
workflow.templates.exports, workflow.templates.accounts are arrays (fallback to
[] if missing) or throw a clear, descriptive error; ensure subsequent uses of
reconciliationsInWorkflow, exportFilesInWorkflow, accountTemplatesInWorkflow,
and calls to listExternallyManagedTemplates/countYamlFiles use these
validated/normalized variables so percentageRoundTwo and .length won't fail.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/cli/stats.js`:
- Around line 23-26: The code currently sets workflowsFolder and populates
workflowHandles using fs.readdirSync which will throw if the "workflows"
directory is missing; update the logic around the
workflowsFolder/workflowHandles assignment (the workflowsFolder const and
workflowHandles variable) to handle a missing folder by either checking
fs.existsSync(workflowsFolder) before calling fs.readdirSync or wrapping
readdirSync in a try/catch and falling back to an empty array, and ensure any
caught error is handled or logged rather than allowed to crash the process.
- Around line 55-58: The TEMPLATE_PATTERN is built from raw template names
(templatesInWorkflow) so regex metacharacters in names break the pattern; before
joining, escape each template string (e.g., implement or use an escapeRegExp
helper and map over templatesInWorkflow to escape chars like . * + ? ^ $ ( ) [ ]
{ } | \ /) then join the escaped names to build TEMPLATE_PATTERN and proceed to
construct YAML_EXPRESSION and fileTypeRegExp from that escaped pattern (refer to
templatesInWorkflow, TEMPLATE_PATTERN, YAML_EXPRESSION, fileTypeRegExp).

In `@lib/utils/fsUtils.js`:
- Around line 483-495: getWorkflow currently uses the raw CLI-provided
workflowHandle to build file paths allowing path traversal; update getWorkflow
to validate and sanitize workflowHandle (e.g., reject any path separators, "..",
or characters outside a safe whitelist like /^[A-Za-z0-9_-]+$/) and return a
clear error instead of accepting unsafe names, then apply the same sanitization
or safe-filename conversion in saveWorkflowOverviewToFile before constructing
CSV/output paths so both functions (getWorkflow and saveWorkflowOverviewToFile)
never join untrusted input into filesystem paths.

---

Nitpick comments:
In `@lib/cli/stats.js`:
- Around line 370-371: The parameter name templatesInWorkflow in function
getYamlSummary is misleading because it actually expects a full workflow object
passed into yamlFilesActivity; rename the parameter to workflow (update the
function signature of getYamlSummary and all internal references) and update any
call sites to pass the same workflow variable name so usage is consistent with
the actual object being passed to yamlFilesActivity.
- Around line 319-344: The code assumes workflow.templates and its
sub-properties exist (e.g., workflow.templates.reconciliations, .exports,
.accounts), which can throw on malformed workflow JSON; update the start of this
block to defensively validate workflow and its template arrays (or normalize
them) before using them: check that workflow is truthy, that workflow.templates
is an object, and that workflow.templates.reconciliations,
workflow.templates.exports, workflow.templates.accounts are arrays (fallback to
[] if missing) or throw a clear, descriptive error; ensure subsequent uses of
reconciliationsInWorkflow, exportFilesInWorkflow, accountTemplatesInWorkflow,
and calls to listExternallyManagedTemplates/countYamlFiles use these
validated/normalized variables so percentageRoundTwo and .length won't fail.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 25e76814-59ef-4417-9c64-b5134a0b6c9a

📥 Commits

Reviewing files that changed from the base of the PR and between 840b02d and e099926.

📒 Files selected for processing (3)
  • bin/cli.js
  • lib/cli/stats.js
  • lib/utils/fsUtils.js

Comment thread lib/cli/stats.js
Comment on lines +23 to +26
} else {
const workflowsFolder = path.join(process.cwd(), "workflows");
workflowHandles = fs.readdirSync(workflowsFolder).map((file) => path.basename(file, ".json"));
}

@coderabbitai coderabbitai Bot Jun 11, 2026

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 | ⚡ Quick win

Missing error handling when workflows folder doesn't exist.

If the workflows directory doesn't exist, fs.readdirSync will throw an uncaught ENOENT error. This could happen in repositories that don't have any workflows defined.

🛡️ Proposed fix to handle missing workflows folder
   } else {
     const workflowsFolder = path.join(process.cwd(), "workflows");
+    if (!fs.existsSync(workflowsFolder)) {
+      consola.error("No workflows folder found. Please create a 'workflows' directory with workflow JSON files.");
+      process.exit(1);
+    }
     workflowHandles = fs.readdirSync(workflowsFolder).map((file) => path.basename(file, ".json"));
+    if (workflowHandles.length === 0) {
+      consola.info("No workflow files found in the workflows folder.");
+      return;
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else {
const workflowsFolder = path.join(process.cwd(), "workflows");
workflowHandles = fs.readdirSync(workflowsFolder).map((file) => path.basename(file, ".json"));
}
} else {
const workflowsFolder = path.join(process.cwd(), "workflows");
if (!fs.existsSync(workflowsFolder)) {
consola.error("No workflows folder found. Please create a 'workflows' directory with workflow JSON files.");
process.exit(1);
}
workflowHandles = fs.readdirSync(workflowsFolder).map((file) => path.basename(file, ".json"));
if (workflowHandles.length === 0) {
consola.info("No workflow files found in the workflows folder.");
return;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/cli/stats.js` around lines 23 - 26, The code currently sets
workflowsFolder and populates workflowHandles using fs.readdirSync which will
throw if the "workflows" directory is missing; update the logic around the
workflowsFolder/workflowHandles assignment (the workflowsFolder const and
workflowHandles variable) to handle a missing folder by either checking
fs.existsSync(workflowsFolder) before calling fs.readdirSync or wrapping
readdirSync in a try/catch and falling back to an empty array, and ensure any
caught error is handled or logged rather than allowed to crash the process.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Additional context: the same readdirSync line also lacks a filter for non-JSON files. Every entry in the folder is mapped through path.basename(file, '.json') — so .DS_Store, README.md, a subdirectory, etc. produce invalid handles (e.g. .DS_Store unchanged). getWorkflow('.DS_Store') then looks for workflows/.DS_Store.json, doesn't find it, and calls process.exit(1), aborting the entire run before any remaining valid workflows are processed. The fix proposed above should also include a .filter(f => f.endsWith('.json')) before the .map(...).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@michieldegezelle, excellent catch — that's a real footgun. Without the filter, even a stray .DS_Store or README.md silently breaks the whole run via process.exit(1) inside getWorkflow. Here's the combined fix:

  } else {
    const workflowsFolder = path.join(process.cwd(), "workflows");
    if (!fs.existsSync(workflowsFolder)) {
      consola.error("No workflows folder found. Please create a 'workflows' directory with workflow JSON files.");
      process.exit(1);
    }
    workflowHandles = fs.readdirSync(workflowsFolder)
+     .filter((file) => file.endsWith(".json"))
      .map((file) => path.basename(file, ".json"));
    if (workflowHandles.length === 0) {
      consola.info("No workflow files found in the workflows folder.");
      return;
    }
  }

The .filter(f => f.endsWith('.json')) ensures that directories, dotfiles (.DS_Store), and other non-JSON files are excluded before path.basename strips the extension — so only valid workflow handles ever reach getWorkflow.


✏️ Learnings added
Learnt from: michieldegezelle
Repo: silverfin/silverfin-cli PR: 247
File: lib/cli/stats.js:23-26
Timestamp: 2026-06-11T07:33:04.929Z
Learning: In `lib/cli/stats.js` (`generateWorkflowOverview`), when reading workflow handles from the `workflows/` folder via `fs.readdirSync`, the result must be filtered to `.json` files only (`.filter(f => f.endsWith('.json'))`) before mapping with `path.basename(file, '.json')`. Without this filter, non-JSON entries like `.DS_Store`, `README.md`, or subdirectories produce invalid handles that cause `getWorkflow()` to call `process.exit(1)`, aborting the entire run.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread lib/cli/stats.js
Comment on lines +55 to 58
const templatesInWorkflow = workflow ? workflow.templates.reconciliations.concat(workflow.templates.accounts) : [];
const TEMPLATE_PATTERN = workflow ? `(${templatesInWorkflow.join("|")})` : `.*`;
const YAML_EXPRESSION = `.*/${TEMPLATE_PATTERN}/tests/.*_liquid_test.*.y(a)?ml`;
const fileTypeRegExp = RegExp(YAML_EXPRESSION, "g");

@coderabbitai coderabbitai Bot Jun 11, 2026

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 | 🟡 Minor | ⚡ Quick win

Regex metacharacters in template names are not escaped.

If template names in the workflow contain regex special characters (e.g., ., +, *, (, )), the constructed regex pattern will behave unexpectedly. Template names are joined and used directly in a regex pattern without escaping.

🔧 Proposed fix to escape regex special characters
+function escapeRegex(string) {
+  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
 async function yamlFilesActivity(sinceDate, workflow) {
   // ...
   const templatesInWorkflow = workflow ? workflow.templates.reconciliations.concat(workflow.templates.accounts) : [];
-  const TEMPLATE_PATTERN = workflow ? `(${templatesInWorkflow.join("|")})` : `.*`;
+  const TEMPLATE_PATTERN = workflow ? `(${templatesInWorkflow.map(escapeRegex).join("|")})` : `.*`;
   const YAML_EXPRESSION = `.*/${TEMPLATE_PATTERN}/tests/.*_liquid_test.*.y(a)?ml`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/cli/stats.js` around lines 55 - 58, The TEMPLATE_PATTERN is built from
raw template names (templatesInWorkflow) so regex metacharacters in names break
the pattern; before joining, escape each template string (e.g., implement or use
an escapeRegExp helper and map over templatesInWorkflow to escape chars like . *
+ ? ^ $ ( ) [ ] { } | \ /) then join the escaped names to build TEMPLATE_PATTERN
and proceed to construct YAML_EXPRESSION and fileTypeRegExp from that escaped
pattern (refer to templatesInWorkflow, TEMPLATE_PATTERN, YAML_EXPRESSION,
fileTypeRegExp).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The identical pattern exists in countYamlFiles (line 99), where TEMPLATE_PATTERN is built the same way from templatesInWorkflow. The escapeRegex helper proposed above needs to be applied there too, otherwise workflow-filtered YAML counts will have the same unescaped-metacharacter exposure as the activity scan.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@michieldegezelle, you're absolutely right — the same unescaped-metacharacter exposure exists in countYamlFiles. Here's the combined fix that defines the helper once and applies it in both places:

+function escapeRegex(string) {
+  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
 async function yamlFilesActivity(sinceDate, workflow) {
   // ...
   const templatesInWorkflow = workflow
     ? workflow.templates.reconciliations.concat(workflow.templates.accounts)
     : [];
-  const TEMPLATE_PATTERN = workflow ? `(${templatesInWorkflow.join("|")})` : `.*`;
+  const TEMPLATE_PATTERN = workflow
+    ? `(${templatesInWorkflow.map(escapeRegex).join("|")})`
+    : `.*`;
   const YAML_EXPRESSION = `.*/${TEMPLATE_PATTERN}/tests/.*_liquid_test.*.y(a)?ml`;
   // ...
 }
 
 async function countYamlFiles(templateType, templatesInWorkflow) {
   // ...
-  const TEMPLATE_PATTERN = templatesInWorkflow ? `(${templatesInWorkflow.join("|")})` : `.*`;
+  const TEMPLATE_PATTERN = templatesInWorkflow
+    ? `(${templatesInWorkflow.map(escapeRegex).join("|")})`
+    : `.*`;
   const YAML_EXPRESSION = `.*${FOLDER}/${TEMPLATE_PATTERN}/tests/.*_liquid_test.*.y(a)?ml`;
   // ...
 }

Both the activity scan and the YAML file count share the same vulnerable pattern-building logic, so both need the same fix. Thanks for catching this!

Comment thread lib/utils/fsUtils.js
Comment on lines +483 to +495
function getWorkflow(workflowHandle) {
try {
const workflowPath = path.join(process.cwd(), "workflows", `${workflowHandle}.json`);
if (!fs.existsSync(workflowPath)) {
throw new Error(`Workflow "${workflowHandle}" not found`);
}
return JSON.parse(fs.readFileSync(workflowPath).toString());
} catch (error) {
consola.error(`An error occurred when trying to read the workflow "${workflowHandle}"`);
consola.error(error);
process.exit(1);
}
}

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 | ⚡ Quick win

Unsanitized workflowHandle from CLI enables path traversal in multiple locations.

Both getWorkflow in lib/utils/fsUtils.js and saveWorkflowOverviewToFile in lib/cli/stats.js use the CLI-provided workflowHandle directly in file paths. The root cause is that the --workflow CLI argument is not validated at the entry point. Consider adding validation in getWorkflow (which is called first) and ensuring safe filenames in the CSV output function.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/utils/fsUtils.js` around lines 483 - 495, getWorkflow currently uses the
raw CLI-provided workflowHandle to build file paths allowing path traversal;
update getWorkflow to validate and sanitize workflowHandle (e.g., reject any
path separators, "..", or characters outside a safe whitelist like
/^[A-Za-z0-9_-]+$/) and return a clear error instead of accepting unsafe names,
then apply the same sanitization or safe-filename conversion in
saveWorkflowOverviewToFile before constructing CSV/output paths so both
functions (getWorkflow and saveWorkflowOverviewToFile) never join untrusted
input into filesystem paths.

Comment thread bin/cli.js
.action((options) => {
stats.generateOverview(options.since);
if (options.workflow) {
const workflowHandle = typeof options.workflow === "string" ? options.workflow : undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

--workflow without a value silently runs all-workflows scan instead of erroring.

The option is declared with optional-argument syntax [handle], so running silverfin stats -s 2024-01-01 --workflow (flag present, no value) causes Commander to set options.workflow = true (boolean). The if (options.workflow) check on the line above is truthy, but this typeof === 'string' guard then sets workflowHandle = undefined. As a result, generateWorkflowOverview(sinceDate, undefined) is called and silently reads every file in the workflows/ folder — the user gets a full-repo scan with no indication their argument was dropped.

Consider using <handle> (required argument) instead of [handle], or explicitly check options.workflow === true and print an error.

Comment thread lib/cli/stats.js
fs.mkdirSync("stats");
}
if (!fs.existsSync(CSV_PATH)) {
fs.writeFileSync(CSV_PATH, ROW_HEADER, (err) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

fs.writeFileSync silently ignores the callback passed as the third argument.

writeFileSync is synchronous and its third parameter is an options object (encoding, mode, flag) — not an error handler. The callback (err) => { consola.error(err); } is coerced to an options object and silently discarded. Any write failure (e.g. permission denied, disk full) throws an uncaught synchronous exception rather than being handled. The same issue exists at line 592 in saveOverviewToFile.

Remove the callback and either let the exception propagate naturally, or wrap the call in a try/catch.

@michieldegezelle michieldegezelle left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A few additional code-review notes not covered by existing threads.

Can you also update the changelog, bump the version in package.json and do a npm update to update the package-lock? Thanks

Comment thread lib/cli/stats.js
// Files to Search (YAML)
const YAML_EXPRESSION = `.*/.*/tests/.*_liquid_test.*.y(a)?ml`;
const templatesInWorkflow = workflow ? workflow.templates.reconciliations.concat(workflow.templates.accounts) : [];
const TEMPLATE_PATTERN = workflow ? `(${templatesInWorkflow.join("|")})` : `.*`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Empty template list produces a broken regex pattern.

When a workflow has no reconciliations and no accounts, templatesInWorkflow is [], so TEMPLATE_PATTERN becomes () and YAML_EXPRESSION effectively matches all YAML test paths — the opposite of filtering. Consider returning early (or skipping YAML activity) when the combined list is empty.

Comment thread lib/cli/stats.js
const FOLDER = fsUtils.FOLDERS[templateType];
const YAML_EXPRESSION = `.*${FOLDER}/.*/tests/.*_liquid_test.*.y(a)?ml`;
const TEMPLATE_PATTERN = templatesInWorkflow ? `(${templatesInWorkflow.join("|")})` : `.*`;
// Issue with counting multiple yml files in the same tests folder?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Leftover open question in a comment (Issue with counting multiple yml files in the same tests folder?). Either confirm this is not a problem and remove the comment, or document/fix the edge case.

Comment thread lib/cli/stats.js
};

// Fetch workflow
// Assume no empty items if present within the workflow.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Global vs workflow stats count different populations.

generateOverview counts only non-empty templates (listNonEmptyTemplates), while workflow mode counts every handle listed in the workflow JSON. That is probably fine, but worth documenting in the CLI help or PR description so users do not compare the two totals directly.

Comment thread lib/cli/stats.js
return summary;
}

async function getYamlSummary(sinceDate, templatesInWorkflow) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor: the second parameter is named templatesInWorkflow but callers pass the full workflow object into yamlFilesActivity. Renaming to workflow would match actual usage.

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.

Workflow test statistics

2 participants