Skip to content

Commit 7f65122

Browse files
committed
Add generating of flow model to data extension editor
This adds the automatic generation of sources/sinks/summary flows to the data extension editor using the flow model queries. This is based on the Python script available in the CodeQL repo. See: https://github.com/github/codeql/blob/main/java/ql/src/utils/modelgenerator/GenerateFlowModel.py
1 parent ef7ee9e commit 7f65122

7 files changed

Lines changed: 282 additions & 5 deletions

File tree

extensions/ql-vscode/src/data-extensions-editor/data-extensions-editor-module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import { CodeQLCliServer } from "../cli";
55
import { QueryRunner } from "../queryRunner";
66
import { DatabaseManager } from "../local-databases";
77
import { extLogger } from "../common";
8+
import { App } from "../common/app";
89

910
export class DataExtensionsEditorModule {
1011
public constructor(
1112
private readonly ctx: ExtensionContext,
13+
private readonly app: App,
1214
private readonly databaseManager: DatabaseManager,
1315
private readonly cliServer: CodeQLCliServer,
1416
private readonly queryRunner: QueryRunner,
@@ -26,6 +28,8 @@ export class DataExtensionsEditorModule {
2628

2729
const view = new DataExtensionsEditorView(
2830
this.ctx,
31+
this.app,
32+
this.databaseManager,
2933
this.cliServer,
3034
this.queryRunner,
3135
this.queryStorageDir,

extensions/ql-vscode/src/data-extensions-editor/data-extensions-editor-view.ts

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,22 @@ import { file } from "tmp-promise";
1818
import { readFile, writeFile } from "fs-extra";
1919
import { dump, load } from "js-yaml";
2020
import { getOnDiskWorkspaceFolders } from "../helpers";
21-
import { DatabaseItem } from "../local-databases";
21+
import { DatabaseItem, DatabaseManager } from "../local-databases";
2222
import { CodeQLCliServer } from "../cli";
23-
import { assertNever } from "../pure/helpers-pure";
23+
import { assertNever, getErrorMessage } from "../pure/helpers-pure";
24+
import { generateFlowModel } from "./generate-flow-model";
25+
import { ModeledMethod } from "./interface";
26+
import { promptImportGithubDatabase } from "../databaseFetcher";
27+
import { App } from "../common/app";
2428

2529
export class DataExtensionsEditorView extends AbstractWebview<
2630
ToDataExtensionsEditorMessage,
2731
FromDataExtensionsEditorMessage
2832
> {
2933
public constructor(
3034
ctx: ExtensionContext,
35+
private readonly app: App,
36+
private readonly databaseManager: DatabaseManager,
3137
private readonly cliServer: CodeQLCliServer,
3238
private readonly queryRunner: QueryRunner,
3339
private readonly queryStorageDir: string,
@@ -69,6 +75,10 @@ export class DataExtensionsEditorView extends AbstractWebview<
6975
await this.saveYaml(msg.yaml);
7076
await this.loadExternalApiUsages();
7177

78+
break;
79+
case "generateExternalApi":
80+
await this.generateExternalApi();
81+
7282
break;
7383
default:
7484
assertNever(msg);
@@ -149,6 +159,84 @@ export class DataExtensionsEditorView extends AbstractWebview<
149159
await this.clearProgress();
150160
}
151161

162+
protected async generateExternalApi(): Promise<void> {
163+
const tokenSource = new CancellationTokenSource();
164+
165+
const selectedDatabase = this.databaseManager.currentDatabaseItem;
166+
167+
const database = await promptImportGithubDatabase(
168+
this.app.commands,
169+
this.databaseManager,
170+
this.app.workspaceStoragePath ?? this.app.globalStoragePath,
171+
this.app.credentials,
172+
(update) => this.showProgress(update),
173+
tokenSource.token,
174+
this.cliServer,
175+
);
176+
if (!database) {
177+
await this.clearProgress();
178+
void extLogger.log("No database chosen");
179+
180+
return;
181+
}
182+
183+
await this.databaseManager.setCurrentDatabaseItem(selectedDatabase);
184+
185+
const workspaceFolder = workspace.workspaceFolders?.find(
186+
(folder) => folder.name === "ql",
187+
);
188+
if (!workspaceFolder) {
189+
void extLogger.log("No workspace folder 'ql' found");
190+
191+
return;
192+
}
193+
194+
await this.showProgress({
195+
step: 0,
196+
maxStep: 4000,
197+
message: "Generating external API",
198+
});
199+
200+
try {
201+
await generateFlowModel(
202+
this.cliServer,
203+
this.queryRunner,
204+
this.queryStorageDir,
205+
workspaceFolder.uri.fsPath,
206+
database,
207+
async (results) => {
208+
const modeledMethodsByName: Record<string, ModeledMethod> = {};
209+
210+
for (const result of results) {
211+
modeledMethodsByName[result[0]] = result[1];
212+
}
213+
214+
await this.postMessage({
215+
t: "addModeledMethods",
216+
modeledMethods: modeledMethodsByName,
217+
});
218+
},
219+
(update) => this.showProgress(update),
220+
tokenSource.token,
221+
);
222+
} catch (e: unknown) {
223+
void extLogger.log(`Error: ${getErrorMessage(e)}`);
224+
}
225+
226+
await this.databaseManager.removeDatabaseItem(
227+
() =>
228+
this.showProgress({
229+
step: 3900,
230+
maxStep: 4000,
231+
message: "Removing temporary database",
232+
}),
233+
tokenSource.token,
234+
database,
235+
);
236+
237+
await this.clearProgress();
238+
}
239+
152240
private async runQuery(): Promise<CoreCompletedQuery | undefined> {
153241
const qlpacks = await qlpackOfDatabase(this.cliServer, this.databaseItem);
154242

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { CancellationToken } from "vscode";
2+
import { DatabaseItem } from "../local-databases";
3+
import { ModeledMethod, ModeledMethodType } from "./interface";
4+
import { join } from "path";
5+
import { QueryRunner } from "../queryRunner";
6+
import { CodeQLCliServer } from "../cli";
7+
import { extLogger, TeeLogger } from "../common";
8+
import { definitions } from "./yaml";
9+
import { ProgressCallback } from "../progress";
10+
import { getOnDiskWorkspaceFolders } from "../helpers";
11+
12+
class FlowModelGenerator {
13+
constructor(
14+
private readonly cli: CodeQLCliServer,
15+
private readonly queryRunner: QueryRunner,
16+
private readonly queryStorageDir: string,
17+
private readonly qlDir: string,
18+
private readonly databaseItem: DatabaseItem,
19+
private readonly progress: ProgressCallback,
20+
private readonly token: CancellationToken,
21+
) {}
22+
23+
async getAddsTo(
24+
type: Exclude<ModeledMethodType, "none">,
25+
queryName: string,
26+
queryStep: number,
27+
): Promise<Array<[string, ModeledMethod]> | undefined> {
28+
const definition = definitions[type];
29+
30+
const query = join(
31+
this.qlDir,
32+
this.databaseItem.language,
33+
"ql/src/utils/modelgenerator",
34+
queryName,
35+
);
36+
37+
const queryRun = this.queryRunner.createQueryRun(
38+
this.databaseItem.databaseUri.fsPath,
39+
{ queryPath: query, quickEvalPosition: undefined },
40+
false,
41+
getOnDiskWorkspaceFolders(),
42+
undefined,
43+
this.queryStorageDir,
44+
undefined,
45+
undefined,
46+
);
47+
48+
const queryResult = await queryRun.evaluate(
49+
({ step, message }) =>
50+
this.progress({
51+
message: `Generating ${type} model: ${message}`,
52+
step: queryStep * 1000 + step,
53+
maxStep: 4000,
54+
}),
55+
this.token,
56+
new TeeLogger(this.queryRunner.logger, queryRun.outputDir.logPath),
57+
);
58+
59+
const bqrsPath = queryResult.outputDir.bqrsPath;
60+
61+
const bqrsInfo = await this.cli.bqrsInfo(bqrsPath);
62+
if (bqrsInfo["result-sets"].length !== 1) {
63+
void extLogger.log(
64+
`Expected exactly one result set, got ${bqrsInfo["result-sets"].length}`,
65+
);
66+
return undefined;
67+
}
68+
69+
const resultSet = bqrsInfo["result-sets"][0];
70+
71+
const decodedResults = await this.cli.bqrsDecode(bqrsPath, resultSet.name);
72+
73+
const results = decodedResults.tuples;
74+
75+
return results
76+
.map((result): [string, ModeledMethod] | undefined => {
77+
const row = result[0] as string;
78+
79+
return definition.readModeledMethod(row.split(";"));
80+
})
81+
.filter(
82+
(result): result is [string, ModeledMethod] => result !== undefined,
83+
);
84+
}
85+
86+
async run(
87+
onResults: (
88+
results: Array<[string, ModeledMethod]>,
89+
) => void | Promise<void>,
90+
) {
91+
const summaryResults = await this.getAddsTo(
92+
"summary",
93+
"CaptureSummaryModels.ql",
94+
0,
95+
);
96+
if (summaryResults) {
97+
await onResults(summaryResults);
98+
}
99+
100+
const sinkResults = await this.getAddsTo("sink", "CaptureSinkModels.ql", 1);
101+
if (sinkResults) {
102+
await onResults(sinkResults);
103+
}
104+
105+
const sourceResults = await this.getAddsTo(
106+
"source",
107+
"CaptureSourceModels.ql",
108+
2,
109+
);
110+
if (sourceResults) {
111+
await onResults(sourceResults);
112+
}
113+
114+
const neutralResults = await this.getAddsTo(
115+
"neutral",
116+
"CaptureNeutralModels.ql",
117+
3,
118+
);
119+
if (neutralResults) {
120+
await onResults(neutralResults);
121+
}
122+
}
123+
}
124+
125+
export async function generateFlowModel(
126+
cli: CodeQLCliServer,
127+
queryRunner: QueryRunner,
128+
queryStorageDir: string,
129+
qlDir: string,
130+
databaseItem: DatabaseItem,
131+
onResults: (results: Array<[string, ModeledMethod]>) => void | Promise<void>,
132+
progress: ProgressCallback,
133+
token: CancellationToken,
134+
) {
135+
const generator = new FlowModelGenerator(
136+
cli,
137+
queryRunner,
138+
queryStorageDir,
139+
qlDir,
140+
databaseItem,
141+
progress,
142+
token,
143+
);
144+
145+
return generator.run(onResults);
146+
}

extensions/ql-vscode/src/data-extensions-editor/yaml.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ function readRowToMethod(row: any[]): string {
1919
return `${row[0]}.${row[1]}#${row[3]}${row[4]}`;
2020
}
2121

22-
const definitions: Record<
22+
export const definitions: Record<
2323
Exclude<ModeledMethodType, "none">,
2424
DataExtensionDefinition
2525
> = {

extensions/ql-vscode/src/extension.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,7 @@ async function activateWithInstalledDistribution(
868868
await ensureDir(dataExtensionsEditorQueryStorageDir);
869869
const dataExtensionsEditorModule = new DataExtensionsEditorModule(
870870
ctx,
871+
app,
871872
dbm,
872873
cliServer,
873874
qs,

extensions/ql-vscode/src/pure/interface-types.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import { RepositoriesFilterSortStateWithIds } from "./variant-analysis-filter-sort";
1616
import { ErrorLike } from "./errors";
1717
import { DataFlowPaths } from "../variant-analysis/shared/data-flow-paths";
18+
import { ModeledMethod } from "../data-extensions-editor/interface";
1819

1920
/**
2021
* This module contains types and code that are shared between
@@ -497,16 +498,27 @@ export interface SetExistingYamlDataMessage {
497498
data: any;
498499
}
499500

501+
export interface AddModeledMethodsMessage {
502+
t: "addModeledMethods";
503+
modeledMethods: Record<string, ModeledMethod>;
504+
}
505+
500506
export interface ApplyDataExtensionYamlMessage {
501507
t: "applyDataExtensionYaml";
502508
yaml: string;
503509
}
504510

511+
export interface GenerateExternalApiMessage {
512+
t: "generateExternalApi";
513+
}
514+
505515
export type ToDataExtensionsEditorMessage =
506516
| SetExternalApiResultsMessage
507517
| ShowProgressMessage
508-
| SetExistingYamlDataMessage;
518+
| SetExistingYamlDataMessage
519+
| AddModeledMethodsMessage;
509520

510521
export type FromDataExtensionsEditorMessage =
511522
| ViewLoadedMsg
512-
| ApplyDataExtensionYamlMessage;
523+
| ApplyDataExtensionYamlMessage
524+
| GenerateExternalApiMessage;

extensions/ql-vscode/src/view/data-extensions-editor/DataExtensionsEditor.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,20 @@ export function DataExtensionsEditor(): JSX.Element {
7474
};
7575
});
7676

77+
break;
78+
case "addModeledMethods":
79+
setModeledMethods((oldModeledMethods) => {
80+
const filteredOldModeledMethods = Object.fromEntries(
81+
Object.entries(oldModeledMethods).filter(
82+
([, value]) => value.type !== "none",
83+
),
84+
);
85+
86+
return {
87+
...msg.modeledMethods,
88+
...filteredOldModeledMethods,
89+
};
90+
});
7791
break;
7892
default:
7993
assertNever(msg);
@@ -168,6 +182,12 @@ export function DataExtensionsEditor(): JSX.Element {
168182
});
169183
}, [methods, modeledMethods]);
170184

185+
const onGenerateClick = useCallback(() => {
186+
vscode.postMessage({
187+
t: "generateExternalApi",
188+
});
189+
}, []);
190+
171191
return (
172192
<DataExtensionsEditorContainer>
173193
{progress.maxStep > 0 && (
@@ -189,6 +209,12 @@ export function DataExtensionsEditor(): JSX.Element {
189209
<div>
190210
<h3>External API modelling</h3>
191211
<VSCodeButton onClick={onApplyClick}>Apply</VSCodeButton>
212+
&nbsp;
213+
<VSCodeButton onClick={onGenerateClick}>
214+
Download and generate
215+
</VSCodeButton>
216+
<br />
217+
<br />
192218
<VSCodeDataGrid>
193219
<VSCodeDataGridRow rowType="header">
194220
<VSCodeDataGridCell cellType="columnheader" gridColumn={1}>

0 commit comments

Comments
 (0)