Skip to content

Commit e95e4a3

Browse files
authored
Merge pull request #2314 from github/koesie10/use-query-in-extension
Add external APIs query in extension
2 parents 013701d + ff405a6 commit e95e4a3

File tree

6 files changed

+285
-73
lines changed

6 files changed

+285
-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
@@ -194,7 +194,6 @@ export class DataExtensionsEditorView extends AbstractWebview<
194194
queryRunner: this.queryRunner,
195195
databaseItem: this.databaseItem,
196196
queryStorageDir: this.queryStorageDir,
197-
logger: extLogger,
198197
progress: (progressUpdate: ProgressUpdate) => {
199198
void this.showProgress(progressUpdate, 1500);
200199
},

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

Lines changed: 51 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
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";
65
import {
76
getOnDiskWorkspaceFolders,
87
showAndLogExceptionWithTelemetry,
98
} from "../helpers";
10-
import { Logger, TeeLogger } from "../common";
9+
import { TeeLogger } from "../common";
1110
import { CancellationToken } from "vscode";
1211
import { CodeQLCliServer } from "../cli";
1312
import { DatabaseItem } from "../local-databases";
1413
import { ProgressCallback } from "../progress";
14+
import { fetchExternalApiQueries } from "./queries";
15+
import { QueryResultType } from "../pure/new-messages";
16+
import { join } from "path";
1517
import { redactableError } from "../pure/errors";
18+
import { QueryLanguage } from "../common/query-language";
1619

1720
export type RunQueryOptions = {
18-
cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveQueriesInSuite">;
21+
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">;
1922
queryRunner: Pick<QueryRunner, "createQueryRun" | "logger">;
2023
databaseItem: Pick<DatabaseItem, "contents" | "databaseUri" | "language">;
2124
queryStorageDir: string;
22-
logger: Logger;
2325

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

39-
const packsToSearch = [qlpacks.dbschemePack];
40-
if (qlpacks.queryPack) {
41-
packsToSearch.push(qlpacks.queryPack);
44+
const query = fetchExternalApiQueries[databaseItem.language as QueryLanguage];
45+
if (!query) {
46+
void showAndLogExceptionWithTelemetry(
47+
redactableError`No external API usage query found for language ${databaseItem.language}`,
48+
);
49+
return;
4250
}
4351

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

6174
const additionalPacks = getOnDiskWorkspaceFolders();
6275
const extensionPacks = Object.keys(
6376
await cliServer.resolveQlpacks(additionalPacks, true),
6477
);
6578

66-
const queries = await cliServer.resolveQueriesInSuite(
67-
suiteFile,
68-
getOnDiskWorkspaceFolders(),
69-
);
70-
71-
if (queries.length !== 1) {
72-
void logger.log(`Expected exactly one query, got ${queries.length}`);
73-
return;
74-
}
75-
76-
const query = queries[0];
77-
7879
const queryRun = queryRunner.createQueryRun(
7980
databaseItem.databaseUri.fsPath,
80-
{ queryPath: query, quickEvalPosition: undefined },
81+
{ queryPath: queryFile, quickEvalPosition: undefined },
8182
false,
8283
getOnDiskWorkspaceFolders(),
8384
extensionPacks,
@@ -86,11 +87,22 @@ export async function runQuery({
8687
undefined,
8788
);
8889

89-
return queryRun.evaluate(
90+
const completedQuery = await queryRun.evaluate(
9091
progress,
9192
token,
9293
new TeeLogger(queryRunner.logger, queryRun.outputDir.logPath),
9394
);
95+
96+
if (completedQuery.resultType !== QueryResultType.SUCCESS) {
97+
void showAndLogExceptionWithTelemetry(
98+
redactableError`External API usage query failed: ${
99+
completedQuery.message ?? "No message"
100+
}`,
101+
);
102+
return;
103+
}
104+
105+
return completedQuery;
94106
}
95107

96108
export type GetResultsOptions = {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { fetchExternalApisQuery as javaFetchExternalApisQuery } from "./java";
2+
import { Query } from "./query";
3+
import { QueryLanguage } from "../../common/query-language";
4+
5+
export const fetchExternalApiQueries: Partial<Record<QueryLanguage, Query>> = {
6+
[QueryLanguage.Java]: javaFetchExternalApisQuery,
7+
};
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)