Skip to content

Commit 5200871

Browse files
committed
Add external APIs query in extension
This adds the external API query text to the extension directly to avoid users having to copy the query to their local `codeql` submodule or having to checkout a specific branch. This is a temporary solution until the queries are stabilized. Once they are, we will upstream these to `github/codeql` and use them like other contextual queries. Since this is just a temporary solution, this is not the prettiest code and is not intended to be a long-term solution. It does not do proper caching and will create a new temporary directory for every query run. The performance hit of this is acceptable and expected.
1 parent 9142fed commit 5200871

File tree

6 files changed

+287
-73
lines changed

6 files changed

+287
-73
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,6 @@ export class DataExtensionsEditorView extends AbstractWebview<
188188
queryRunner: this.queryRunner,
189189
databaseItem: this.databaseItem,
190190
queryStorageDir: this.queryStorageDir,
191-
logger: extLogger,
192191
progress: (progressUpdate: ProgressUpdate) => {
193192
void this.showProgress(progressUpdate, 1500);
194193
},

extensions/ql-vscode/src/data-extensions-editor/external-api-usage-query.ts

Lines changed: 54 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
import { CoreCompletedQuery, QueryRunner } from "../queryRunner";
2-
import { qlpackOfDatabase } from "../contextual/queryResolver";
3-
import { file } from "tmp-promise";
2+
import { dir } from "tmp-promise";
43
import { writeFile } from "fs-extra";
54
import { dump as dumpYaml } from "js-yaml";
6-
import { getOnDiskWorkspaceFolders } from "../helpers";
5+
import {
6+
getOnDiskWorkspaceFolders,
7+
showAndLogExceptionWithTelemetry,
8+
} from "../helpers";
79
import { Logger, TeeLogger } from "../common";
810
import { CancellationToken } from "vscode";
911
import { CodeQLCliServer } from "../cli";
1012
import { DatabaseItem } from "../local-databases";
1113
import { ProgressCallback } from "../progress";
14+
import { fetchExternalApiQueries } from "./queries";
15+
import { QueryResultType } from "../pure/new-messages";
16+
import { join } from "path";
17+
import { redactableError } from "../pure/errors";
1218

1319
export type RunQueryOptions = {
14-
cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveQueriesInSuite">;
20+
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">;
1521
queryRunner: Pick<QueryRunner, "createQueryRun" | "logger">;
1622
databaseItem: Pick<DatabaseItem, "contents" | "databaseUri" | "language">;
1723
queryStorageDir: string;
18-
logger: Logger;
1924

2025
progress: ProgressCallback;
2126
token: CancellationToken;
@@ -26,54 +31,53 @@ export async function runQuery({
2631
queryRunner,
2732
databaseItem,
2833
queryStorageDir,
29-
logger,
3034
progress,
3135
token,
3236
}: RunQueryOptions): Promise<CoreCompletedQuery | undefined> {
33-
const qlpacks = await qlpackOfDatabase(cliServer, databaseItem);
37+
// The below code is temporary to allow for rapid prototyping of the queries. Once the queries are stabilized, we will
38+
// move these queries into the `github/codeql` repository and use them like any other contextual (e.g. AST) queries.
39+
// This is intentionally not pretty code, as it will be removed soon.
40+
// For a reference of what this should do in the future, see the previous implementation in
41+
// https://github.com/github/vscode-codeql/blob/089d3566ef0bc67d9b7cc66e8fd6740b31c1c0b0/extensions/ql-vscode/src/data-extensions-editor/external-api-usage-query.ts#L33-L72
3442

35-
const packsToSearch = [qlpacks.dbschemePack];
36-
if (qlpacks.queryPack) {
37-
packsToSearch.push(qlpacks.queryPack);
43+
const query = fetchExternalApiQueries[databaseItem.language];
44+
if (!query) {
45+
void showAndLogExceptionWithTelemetry(
46+
redactableError`No external API usage query found for language ${databaseItem.language}`,
47+
);
48+
return;
3849
}
3950

40-
const suiteFile = (
41-
await file({
42-
postfix: ".qls",
43-
})
44-
).path;
45-
const suiteYaml = [];
46-
for (const qlpack of packsToSearch) {
47-
suiteYaml.push({
48-
from: qlpack,
49-
queries: ".",
50-
include: {
51-
id: `${databaseItem.language}/telemetry/fetch-external-apis`,
52-
},
53-
});
51+
const queryDir = (await dir({ unsafeCleanup: true })).path;
52+
const queryFile = join(queryDir, "FetchExternalApis.ql");
53+
await writeFile(queryFile, query.mainQuery, "utf8");
54+
55+
if (query.dependencies) {
56+
for (const [filename, contents] of Object.entries(query.dependencies)) {
57+
const dependencyFile = join(queryDir, filename);
58+
await writeFile(dependencyFile, contents, "utf8");
59+
}
5460
}
55-
await writeFile(suiteFile, dumpYaml(suiteYaml), "utf8");
61+
62+
const syntheticQueryPack = {
63+
name: "codeql/external-api-usage",
64+
version: "0.0.0",
65+
dependencies: {
66+
[`codeql/${databaseItem.language}-all`]: "*",
67+
},
68+
};
69+
70+
const qlpackFile = join(queryDir, "codeql-pack.yml");
71+
await writeFile(qlpackFile, dumpYaml(syntheticQueryPack), "utf8");
5672

5773
const additionalPacks = getOnDiskWorkspaceFolders();
5874
const extensionPacks = Object.keys(
5975
await cliServer.resolveQlpacks(additionalPacks, true),
6076
);
6177

62-
const queries = await cliServer.resolveQueriesInSuite(
63-
suiteFile,
64-
getOnDiskWorkspaceFolders(),
65-
);
66-
67-
if (queries.length !== 1) {
68-
void logger.log(`Expected exactly one query, got ${queries.length}`);
69-
return;
70-
}
71-
72-
const query = queries[0];
73-
7478
const queryRun = queryRunner.createQueryRun(
7579
databaseItem.databaseUri.fsPath,
76-
{ queryPath: query, quickEvalPosition: undefined },
80+
{ queryPath: queryFile, quickEvalPosition: undefined },
7781
false,
7882
getOnDiskWorkspaceFolders(),
7983
extensionPacks,
@@ -82,11 +86,22 @@ export async function runQuery({
8286
undefined,
8387
);
8488

85-
return queryRun.evaluate(
89+
const completedQuery = await queryRun.evaluate(
8690
progress,
8791
token,
8892
new TeeLogger(queryRunner.logger, queryRun.outputDir.logPath),
8993
);
94+
95+
if (completedQuery.resultType !== QueryResultType.SUCCESS) {
96+
void showAndLogExceptionWithTelemetry(
97+
redactableError`External API usage query failed: ${
98+
completedQuery.message ?? "No message"
99+
}`,
100+
);
101+
return;
102+
}
103+
104+
return completedQuery;
90105
}
91106

92107
export type GetResultsOptions = {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { fetchExternalApisQuery as javaFetchExternalApisQuery } from "./java";
2+
import { Query } from "./query";
3+
4+
export const fetchExternalApiQueries: Record<string, Query> = {
5+
java: javaFetchExternalApisQuery,
6+
};
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { Query } from "./query";
2+
3+
export const fetchExternalApisQuery: Query = {
4+
mainQuery: `/**
5+
* @name Usage of APIs coming from external libraries
6+
* @description A list of 3rd party APIs used in the codebase. Excludes test and generated code.
7+
* @tags telemetry
8+
* @id java/telemetry/fetch-external-apis
9+
*/
10+
11+
import java
12+
import semmle.code.java.dataflow.internal.FlowSummaryImpl as FlowSummaryImpl
13+
import ExternalApi
14+
15+
private Call aUsage(ExternalApi api) {
16+
result.getCallee().getSourceDeclaration() = api and
17+
not result.getFile() instanceof GeneratedFile
18+
}
19+
20+
private boolean isSupported(ExternalApi api) {
21+
api.isSupported() and result = true
22+
or
23+
api = any(FlowSummaryImpl::Public::NeutralCallable nsc).asCallable() and result = true
24+
or
25+
not api.isSupported() and
26+
not api = any(FlowSummaryImpl::Public::NeutralCallable nsc).asCallable() and
27+
result = false
28+
}
29+
30+
from ExternalApi api, string apiName, boolean supported, Call usage
31+
where
32+
apiName = api.getApiName() and
33+
supported = isSupported(api) and
34+
usage = aUsage(api)
35+
select apiName, supported, usage
36+
`,
37+
dependencies: {
38+
"ExternalApi.qll": `/** Provides classes and predicates related to handling APIs from external libraries. */
39+
40+
private import java
41+
private import semmle.code.java.dataflow.DataFlow
42+
private import semmle.code.java.dataflow.ExternalFlow
43+
private import semmle.code.java.dataflow.FlowSources
44+
private import semmle.code.java.dataflow.FlowSummary
45+
private import semmle.code.java.dataflow.internal.DataFlowPrivate
46+
private import semmle.code.java.dataflow.TaintTracking
47+
48+
pragma[nomagic]
49+
private predicate isTestPackage(Package p) {
50+
p.getName()
51+
.matches([
52+
"org.junit%", "junit.%", "org.mockito%", "org.assertj%",
53+
"com.github.tomakehurst.wiremock%", "org.hamcrest%", "org.springframework.test.%",
54+
"org.springframework.mock.%", "org.springframework.boot.test.%", "reactor.test%",
55+
"org.xmlunit%", "org.testcontainers.%", "org.opentest4j%", "org.mockserver%",
56+
"org.powermock%", "org.skyscreamer.jsonassert%", "org.rnorth.visibleassertions",
57+
"org.openqa.selenium%", "com.gargoylesoftware.htmlunit%", "org.jboss.arquillian.testng%",
58+
"org.testng%"
59+
])
60+
}
61+
62+
/**
63+
* A test library.
64+
*/
65+
private class TestLibrary extends RefType {
66+
TestLibrary() { isTestPackage(this.getPackage()) }
67+
}
68+
69+
private string containerAsJar(Container container) {
70+
if container instanceof JarFile then result = container.getBaseName() else result = "rt.jar"
71+
}
72+
73+
/** Holds if the given callable is not worth supporting. */
74+
private predicate isUninteresting(Callable c) {
75+
c.getDeclaringType() instanceof TestLibrary or
76+
c.(Constructor).isParameterless()
77+
}
78+
79+
/**
80+
* An external API from either the Standard Library or a 3rd party library.
81+
*/
82+
class ExternalApi extends Callable {
83+
ExternalApi() { not this.fromSource() and not isUninteresting(this) }
84+
85+
/**
86+
* Gets information about the external API in the form expected by the MaD modeling framework.
87+
*/
88+
string getApiName() {
89+
result =
90+
this.getDeclaringType().getPackage() + "." + this.getDeclaringType().getSourceDeclaration() +
91+
"#" + this.getName() + paramsString(this)
92+
}
93+
94+
/**
95+
* Gets the jar file containing this API. Normalizes the Java Runtime to "rt.jar" despite the presence of modules.
96+
*/
97+
string jarContainer() { result = containerAsJar(this.getCompilationUnit().getParentContainer*()) }
98+
99+
/** Gets a node that is an input to a call to this API. */
100+
private DataFlow::Node getAnInput() {
101+
exists(Call call | call.getCallee().getSourceDeclaration() = this |
102+
result.asExpr().(Argument).getCall() = call or
103+
result.(ArgumentNode).getCall().asCall() = call
104+
)
105+
}
106+
107+
/** Gets a node that is an output from a call to this API. */
108+
private DataFlow::Node getAnOutput() {
109+
exists(Call call | call.getCallee().getSourceDeclaration() = this |
110+
result.asExpr() = call or
111+
result.(DataFlow::PostUpdateNode).getPreUpdateNode().(ArgumentNode).getCall().asCall() = call
112+
)
113+
}
114+
115+
/** Holds if this API has a supported summary. */
116+
pragma[nomagic]
117+
predicate hasSummary() {
118+
this = any(SummarizedCallable sc).asCallable() or
119+
TaintTracking::localAdditionalTaintStep(this.getAnInput(), _)
120+
}
121+
122+
pragma[nomagic]
123+
predicate isSource() {
124+
this.getAnOutput() instanceof RemoteFlowSource or sourceNode(this.getAnOutput(), _)
125+
}
126+
127+
/** Holds if this API is a known sink. */
128+
pragma[nomagic]
129+
predicate isSink() { sinkNode(this.getAnInput(), _) }
130+
131+
/** Holds if this API is supported by existing CodeQL libraries, that is, it is either a recognized source or sink or has a flow summary. */
132+
predicate isSupported() { this.hasSummary() or this.isSource() or this.isSink() }
133+
}
134+
135+
/** DEPRECATED: Alias for ExternalApi */
136+
deprecated class ExternalAPI = ExternalApi;
137+
138+
/**
139+
* Gets the limit for the number of results produced by a telemetry query.
140+
*/
141+
int resultLimit() { result = 1000 }
142+
143+
/**
144+
* Holds if it is relevant to count usages of \`api\`.
145+
*/
146+
signature predicate relevantApi(ExternalApi api);
147+
148+
/**
149+
* Given a predicate to count relevant API usages, this module provides a predicate
150+
* for restricting the number or returned results based on a certain limit.
151+
*/
152+
module Results<relevantApi/1 getRelevantUsages> {
153+
private int getUsages(string apiName) {
154+
result =
155+
strictcount(Call c, ExternalApi api |
156+
c.getCallee().getSourceDeclaration() = api and
157+
not c.getFile() instanceof GeneratedFile and
158+
apiName = api.getApiName() and
159+
getRelevantUsages(api)
160+
)
161+
}
162+
163+
private int getOrder(string apiInfo) {
164+
apiInfo =
165+
rank[result](string info, int usages |
166+
usages = getUsages(info)
167+
|
168+
info order by usages desc, info
169+
)
170+
}
171+
172+
/**
173+
* Holds if there exists an API with \`apiName\` that is being used \`usages\` times
174+
* and if it is in the top results (guarded by resultLimit).
175+
*/
176+
predicate restrict(string apiName, int usages) {
177+
usages = getUsages(apiName) and
178+
getOrder(apiName) <= resultLimit()
179+
}
180+
}
181+
`,
182+
},
183+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export type Query = {
2+
mainQuery: string;
3+
dependencies?: {
4+
[filename: string]: string;
5+
};
6+
};

0 commit comments

Comments
 (0)