Skip to content

Commit 336195c

Browse files
committed
adds exportSarif method
1 parent 059bc80 commit 336195c

File tree

4 files changed

+141
-34
lines changed

4 files changed

+141
-34
lines changed

README.md

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,23 @@
44
</a>
55
</p>
66

7-
<p align="center"><i>A plug-and-play engine for Flow metadata in Node.js & browsers—with 20+ rules to catch unsafe contexts, loop queries, hardcoded IDs, and more.</i></p>
7+
<p align="center"><i>UMD-compatible Flow metadata engine for Node.js & browsers—20+ rules to catch common issues.</i></p>
88

9-
- [Default Rules](#default-rules)
10-
- [Configuration](#configuration)
9+
---
10+
11+
## Table of contens
12+
13+
- **[Default Rules](#default-rules)**
14+
- **[Configuration](#configuration)**
1115
- [Defining Severity Levels](#defining-severity-levels)
1216
- [Configuring Expressions](#configuring-expressions)
1317
- [Specifying Exceptions](#specifying-exceptions)
1418
- [Include Beta Rules](#include-beta-rules)
15-
- [Usage](#Usage)
16-
- [Installation](#installation)
17-
- [Core Functions](#core-functions)
18-
- [Development](#development)
19+
- **[Usage](#Usage)**
20+
- [Examples](#examples)
21+
- [Functions](#core-functions)
22+
- **[Installation](#installation)**
23+
- **[Development](#development)**
1924

2025
---
2126

@@ -24,7 +29,7 @@
2429
<p>📌 <strong>Tip:</strong> To link directly to a specific rule, use the full GitHub anchor link format. Example:</p>
2530
<p><em><a href="https://github.com/Flow-Scanner/lightning-flow-scanner-core#unsafe-running-context">https://github.com/Flow-Scanner/lightning-flow-scanner-core#unsafe-running-context</a></em></i></p>
2631

27-
### Action Calls In Loop
32+
### Action Calls In Loop(Beta)
2833

2934
_[ActionCallsInLoop](https://github.com/Flow-Scanner/lightning-flow-scanner-core/tree/main/src/main/rules/ActionCallsInLoop.ts)_ - To prevent exceeding Apex governor limits, it is advisable to consolidate and bulkify your apex calls, utilizing a single action call containing a collection variable at the end of the loop.
3035

@@ -226,31 +231,62 @@ New rules are introduced in Beta mode before being added to the default ruleset.
226231

227232
## Usage
228233

229-
### Installation
234+
The Lightning Flow Scanner Core can be used as a dependency in Node.js and browser environments, or used as a standalone UMD module.
230235

231-
The Lightning Flow Scanner Core can be used as a dependency in Node.js and browser environments, or used as a standalone UMD module. To install:
236+
### Examples
232237

233-
```bash
234-
npm install @flow-scanner/lightning-flow-scanner-core
238+
```js
239+
// Basic
240+
import { parse, scan } from "@flow-scanner/lightning-flow-scanner-core";
241+
parse("flows/*.xml").then(scan);
242+
243+
// Apply fixes automatically
244+
import { parse, scan, fix } from "@flow-scanner/lightning-flow-scanner-core";
245+
parse("flows/*.xml").then(scan).then(fix);
246+
247+
// Get SARIF output
248+
import { parse, scan, exportSarif } from "@flow-scanner/lightning-flow-scanner-core";
249+
parse("flows/*.xml")
250+
.then(scan)
251+
.then(exportSarif)
252+
.then((sarif) => save("results.sarif", sarif));
235253
```
236254

237-
### Core Functions
255+
### Functions
238256

239257
#### [`getRules(ruleNames?: string[]): IRuleDefinition[]`](https://github.com/Flow-Scanner/lightning-flow-scanner-core/tree/main/src/main/libs/GetRuleDefinitions.ts)
240258

241259
_Retrieves rule definitions used in the scanner._
242260

243261
#### [`parse(selectedUris: any): Promise<ParsedFlow[]>`](https://github.com/Flow-Scanner/lightning-flow-scanner-core/tree/main/src/main/libs/ParseFlows.ts)
244262

245-
_Parses metadata from selected Flow files._
263+
_Loads Flow XML files into in-memory models._
246264

247265
#### [`scan(parsedFlows: ParsedFlow[], ruleOptions?: IRulesConfig): ScanResult[]`](https://github.com/Flow-Scanner/lightning-flow-scanner-core/tree/main/src/main/libs/ScanFlows.ts)
248266

249-
_Runs rules against parsed flows and returns scan results._
267+
_Runs all enabled rules and returns detailed violations._
250268

251269
#### [`fix(results: ScanResult[]): ScanResult[]`](https://github.com/Flow-Scanner/lightning-flow-scanner-core/tree/main/src/main/libs/FixFlows.ts)
252270

253-
_Attempts to apply automatic fixes where available._
271+
_Automatically applies available fixes(removing variables and unconnected elements)._
272+
273+
#### [`exportSarif(results: ScanResult[]): string`](https://github.com/Flow-Scanner/lightning-flow-scanner-core/tree/main/src/main/libs/ExportSarif.ts)
274+
275+
_Generates SARIF output with paths and exact line numbers._
276+
277+
---
278+
279+
## Installation
280+
281+
`lightning-flow-scanner-core` is published to **npm** only.
282+
283+
[![npm version](https://img.shields.io/npm/v/@flow-scanner/lightning-flow-scanner-core?label=npm)](https://www.npmjs.com/package/@flow-scanner/lightning-flow-scanner-core)
284+
285+
**To install with npm:**
286+
287+
```bash
288+
npm install @flow-scanner/lightning-flow-scanner-core
289+
```
254290

255291
---
256292

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import type { IRuleDefinition } from "./main/interfaces/IRuleDefinition";
22
import type { IRulesConfig } from "./main/interfaces/IRulesConfig";
33

44
import { Compiler } from "./main/libs/Compiler";
5+
import { exportSarif } from "./main/libs/ExportSarif";
56
import { fix } from "./main/libs/FixFlows";
67
import { getBetaRules, getRules } from "./main/libs/GetRuleDefinitions";
78
import { parse } from "./main/libs/ParseFlows";
8-
import { exportSarif } from "./main/libs/SARIFExporter";
99
import { scan } from "./main/libs/ScanFlows";
1010
import { AdvancedRule } from "./main/models/AdvancedRule";
1111
import { Flow } from "./main/models/Flow";

src/main/libs/SarifExporter.ts renamed to src/main/libs/ExportSarif.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1+
// src/main/libs/exportSarif.ts
12
import { Flow } from "../models/Flow";
23
import { ResultDetails } from "../models/ResultDetails";
34
import { ScanResult } from "../models/ScanResult";
45

5-
/**
6-
* Export scan results to SARIF v2.1.0
7-
* Uses real fsPath → GitHub clickable
8-
* Falls back to virtual URI in browser
9-
*/
106
export function exportSarif(results: ScanResult[]): string {
117
const runs = results.map((result) => {
128
const flow = result.flow;
@@ -21,7 +17,7 @@ export function exportSarif(results: ScanResult[]): string {
2117
locations: [{
2218
physicalLocation: {
2319
artifactLocation: { index: 0, uri },
24-
region: mapRegion(d),
20+
region: mapRegion(d, result.flow.toXMLString() || "")
2521
},
2622
}],
2723
message: { text: r.errorMessage || `${r.ruleName} in ${d.name}` },
@@ -42,7 +38,6 @@ export function exportSarif(results: ScanResult[]): string {
4238
.map(r => ({
4339
defaultConfiguration: { level: mapSeverity(r.severity) },
4440
fullDescription: { text: r.ruleDefinition.description || "" },
45-
helpUri: r.ruleDefinition.helpUrl,
4641
id: r.ruleName,
4742
shortDescription: { text: r.ruleDefinition.description || r.ruleName },
4843
})),
@@ -59,27 +54,33 @@ export function exportSarif(results: ScanResult[]): string {
5954
}, null, 2);
6055
}
6156

62-
// ─── Private Helpers ───
6357
function getUri(flow: Flow): string {
6458
return flow.fsPath
6559
? flow.fsPath.replace(/\\/g, "/")
6660
: `flows/${flow.name}.flow-meta.xml`;
6761
}
6862

69-
function mapRegion(detail: ResultDetails): any {
70-
if (detail.metaType === "node" && (detail.details as any).locationY != null) {
71-
return {
72-
startColumn: (detail.details as any).locationX || 1,
73-
startLine: Math.max(1, (detail.details as any).locationY),
74-
};
63+
function mapRegion(detail: ResultDetails, rawXml: string = ""): any {
64+
if (!rawXml) return { startLine: 1, startColumn: 1 };
65+
66+
const lines = rawXml.split("\n");
67+
const name = detail.name;
68+
69+
for (let i = 0; i < lines.length; i++) {
70+
if (lines[i].includes(`<name>${name}</name>`)) {
71+
return {
72+
startLine: i + 1,
73+
startColumn: lines[i].indexOf(name) + 1
74+
};
75+
}
7576
}
76-
return { startColumn: 1, startLine: 1 };
77+
return { startLine: 1, startColumn: 1 };
7778
}
78-
7979
function mapSeverity(sev: string): "error" | "note" | "warning" {
8080
switch (sev?.toLowerCase()) {
8181
case "info":
82-
case "note": return "note"; case "warning": return "warning";
82+
case "note": return "note";
83+
case "warning": return "warning";
8384
default: return "error";
8485
}
8586
}

tests/ExportSarif.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// __tests__/exportSarif.test.ts
2+
import { describe, expect, it } from "@jest/globals";
3+
import * as path from "path";
4+
import * as core from "../src";
5+
6+
describe("exportSarif()", () => {
7+
const badFlowPath = path.join(__dirname, "../assets/example-flows/force-app/main/default/flows/DML_Statement_In_A_Loop.flow-meta.xml");
8+
const goodFlowPath = path.join(__dirname, "../assets/example-flows/force-app/main/default/flows/Duplicate_DML_Operation_Fixed.flow-meta.xml");
9+
10+
const config = {
11+
rules: {
12+
DMLStatementInLoop: { severity: "error" },
13+
},
14+
};
15+
16+
it("generates valid SARIF with real file path and line numbers", async () => {
17+
const flows = await core.parse([badFlowPath]);
18+
const results = core.scan(flows, config);
19+
const sarif = core.exportSarif(results);
20+
21+
const json = JSON.parse(sarif);
22+
23+
// SARIF structure
24+
expect(json.version).toBe("2.1.0");
25+
expect(json.runs).toHaveLength(1);
26+
expect(json.runs[0].tool.driver.name).toBe("Lightning Flow Scanner");
27+
28+
// Artifacts: real path
29+
const artifactUri = json.runs[0].artifacts[0].location.uri;
30+
expect(artifactUri).toContain("force-app/main/default/flows/DML_Statement_In_A_Loop.flow-meta.xml");
31+
32+
// Results: one issue
33+
const resultsArray = json.runs[0].results;
34+
expect(resultsArray).toHaveLength(1);
35+
expect(resultsArray[0].ruleId).toBe("DMLStatementInLoop");
36+
expect(resultsArray[0].level).toBe("error");
37+
38+
// Location: has region
39+
const region = resultsArray[0].locations[0].physicalLocation.region;
40+
expect(region).toBeDefined();
41+
expect(typeof region.startLine).toBe("number");
42+
expect(typeof region.startColumn).toBe("number");
43+
expect(region.startLine).toBeGreaterThanOrEqual(1);
44+
expect(region.startColumn).toBeGreaterThanOrEqual(1);
45+
46+
// Message
47+
expect(resultsArray[0].message.text).toContain("createNewCase");
48+
});
49+
50+
it("generates empty results for fixed flow", async () => {
51+
const flows = await core.parse([goodFlowPath]);
52+
const results = core.scan(flows, config);
53+
const sarif = core.exportSarif(results);
54+
55+
const json = JSON.parse(sarif);
56+
expect(json.runs[0].results).toHaveLength(0);
57+
});
58+
59+
it("falls back to virtual URI when no fsPath", async () => {
60+
const flows = await core.parse([badFlowPath]);
61+
// Simulate browser: remove fsPath
62+
flows[0].flow.fsPath = undefined;
63+
const results = core.scan(flows, config);
64+
const sarif = core.exportSarif(results);
65+
const json = JSON.parse(sarif);
66+
67+
const uri = json.runs[0].artifacts[0].location.uri;
68+
expect(uri).toBe("flows/DML_Statement_In_A_Loop.flow-meta.xml");
69+
});
70+
});

0 commit comments

Comments
 (0)