Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,6 @@ export class DataExtensionsEditorView extends AbstractWebview<
queryRunner: this.queryRunner,
databaseItem: this.databaseItem,
queryStorageDir: this.queryStorageDir,
logger: extLogger,
progress: (progressUpdate: ProgressUpdate) => {
void this.showProgress(progressUpdate, 1500);
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
import { CoreCompletedQuery, QueryRunner } from "../queryRunner";
import { qlpackOfDatabase } from "../contextual/queryResolver";
import { file } from "tmp-promise";
import { dir } from "tmp-promise";
import { writeFile } from "fs-extra";
import { dump as dumpYaml } from "js-yaml";
import {
getOnDiskWorkspaceFolders,
showAndLogExceptionWithTelemetry,
} from "../helpers";
import { Logger, TeeLogger } from "../common";
import { TeeLogger } from "../common";
import { CancellationToken } from "vscode";
import { CodeQLCliServer } from "../cli";
import { DatabaseItem } from "../local-databases";
import { ProgressCallback } from "../progress";
import { fetchExternalApiQueries } from "./queries";
import { QueryResultType } from "../pure/new-messages";
import { join } from "path";
import { redactableError } from "../pure/errors";
import { QueryLanguage } from "../common/query-language";

export type RunQueryOptions = {
cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveQueriesInSuite">;
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">;
queryRunner: Pick<QueryRunner, "createQueryRun" | "logger">;
databaseItem: Pick<DatabaseItem, "contents" | "databaseUri" | "language">;
queryStorageDir: string;
logger: Logger;

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

const packsToSearch = [qlpacks.dbschemePack];
if (qlpacks.queryPack) {
packsToSearch.push(qlpacks.queryPack);
const query = fetchExternalApiQueries[databaseItem.language as QueryLanguage];
if (!query) {
void showAndLogExceptionWithTelemetry(
redactableError`No external API usage query found for language ${databaseItem.language}`,
);
return;
}

const suiteFile = (
await file({
postfix: ".qls",
})
).path;
const suiteYaml = [];
for (const qlpack of packsToSearch) {
suiteYaml.push({
from: qlpack,
queries: ".",
include: {
id: `${databaseItem.language}/telemetry/fetch-external-apis`,
},
});
const queryDir = (await dir({ unsafeCleanup: true })).path;
const queryFile = join(queryDir, "FetchExternalApis.ql");
await writeFile(queryFile, query.mainQuery, "utf8");

if (query.dependencies) {
for (const [filename, contents] of Object.entries(query.dependencies)) {
const dependencyFile = join(queryDir, filename);
await writeFile(dependencyFile, contents, "utf8");
}
}
await writeFile(suiteFile, dumpYaml(suiteYaml), "utf8");

const syntheticQueryPack = {
name: "codeql/external-api-usage",
version: "0.0.0",
dependencies: {
[`codeql/${databaseItem.language}-all`]: "*",
},
};

const qlpackFile = join(queryDir, "codeql-pack.yml");
await writeFile(qlpackFile, dumpYaml(syntheticQueryPack), "utf8");

const additionalPacks = getOnDiskWorkspaceFolders();
const extensionPacks = Object.keys(
await cliServer.resolveQlpacks(additionalPacks, true),
);

const queries = await cliServer.resolveQueriesInSuite(
suiteFile,
getOnDiskWorkspaceFolders(),
);

if (queries.length !== 1) {
void logger.log(`Expected exactly one query, got ${queries.length}`);
return;
}

const query = queries[0];

const queryRun = queryRunner.createQueryRun(
databaseItem.databaseUri.fsPath,
{ queryPath: query, quickEvalPosition: undefined },
{ queryPath: queryFile, quickEvalPosition: undefined },
false,
getOnDiskWorkspaceFolders(),
extensionPacks,
Expand All @@ -86,11 +87,22 @@ export async function runQuery({
undefined,
);

return queryRun.evaluate(
const completedQuery = await queryRun.evaluate(
progress,
token,
new TeeLogger(queryRunner.logger, queryRun.outputDir.logPath),
);

if (completedQuery.resultType !== QueryResultType.SUCCESS) {
void showAndLogExceptionWithTelemetry(
redactableError`External API usage query failed: ${
completedQuery.message ?? "No message"
}`,
);
return;
}

return completedQuery;
}

export type GetResultsOptions = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { fetchExternalApisQuery as javaFetchExternalApisQuery } from "./java";
import { Query } from "./query";
import { QueryLanguage } from "../../common/query-language";

export const fetchExternalApiQueries: Partial<Record<QueryLanguage, Query>> = {
[QueryLanguage.Java]: javaFetchExternalApisQuery,
};
183 changes: 183 additions & 0 deletions extensions/ql-vscode/src/data-extensions-editor/queries/java.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { Query } from "./query";

export const fetchExternalApisQuery: Query = {
mainQuery: `/**
* @name Usage of APIs coming from external libraries
* @description A list of 3rd party APIs used in the codebase. Excludes test and generated code.
* @tags telemetry
* @id java/telemetry/fetch-external-apis
*/

import java
import semmle.code.java.dataflow.internal.FlowSummaryImpl as FlowSummaryImpl
import ExternalApi

private Call aUsage(ExternalApi api) {
result.getCallee().getSourceDeclaration() = api and
not result.getFile() instanceof GeneratedFile
}

private boolean isSupported(ExternalApi api) {
api.isSupported() and result = true
or
api = any(FlowSummaryImpl::Public::NeutralCallable nsc).asCallable() and result = true
or
not api.isSupported() and
not api = any(FlowSummaryImpl::Public::NeutralCallable nsc).asCallable() and
result = false
}

from ExternalApi api, string apiName, boolean supported, Call usage
where
apiName = api.getApiName() and
supported = isSupported(api) and
usage = aUsage(api)
select apiName, supported, usage
`,
dependencies: {
"ExternalApi.qll": `/** Provides classes and predicates related to handling APIs from external libraries. */

private import java
private import semmle.code.java.dataflow.DataFlow
private import semmle.code.java.dataflow.ExternalFlow
private import semmle.code.java.dataflow.FlowSources
private import semmle.code.java.dataflow.FlowSummary
private import semmle.code.java.dataflow.internal.DataFlowPrivate
private import semmle.code.java.dataflow.TaintTracking

pragma[nomagic]
private predicate isTestPackage(Package p) {
p.getName()
.matches([
"org.junit%", "junit.%", "org.mockito%", "org.assertj%",
"com.github.tomakehurst.wiremock%", "org.hamcrest%", "org.springframework.test.%",
"org.springframework.mock.%", "org.springframework.boot.test.%", "reactor.test%",
"org.xmlunit%", "org.testcontainers.%", "org.opentest4j%", "org.mockserver%",
"org.powermock%", "org.skyscreamer.jsonassert%", "org.rnorth.visibleassertions",
"org.openqa.selenium%", "com.gargoylesoftware.htmlunit%", "org.jboss.arquillian.testng%",
"org.testng%"
])
}

/**
* A test library.
*/
private class TestLibrary extends RefType {
TestLibrary() { isTestPackage(this.getPackage()) }
}

private string containerAsJar(Container container) {
if container instanceof JarFile then result = container.getBaseName() else result = "rt.jar"
}

/** Holds if the given callable is not worth supporting. */
private predicate isUninteresting(Callable c) {
c.getDeclaringType() instanceof TestLibrary or
c.(Constructor).isParameterless()
}

/**
* An external API from either the Standard Library or a 3rd party library.
*/
class ExternalApi extends Callable {
ExternalApi() { not this.fromSource() and not isUninteresting(this) }

/**
* Gets information about the external API in the form expected by the MaD modeling framework.
*/
string getApiName() {
result =
this.getDeclaringType().getPackage() + "." + this.getDeclaringType().getSourceDeclaration() +
"#" + this.getName() + paramsString(this)
}

/**
* Gets the jar file containing this API. Normalizes the Java Runtime to "rt.jar" despite the presence of modules.
*/
string jarContainer() { result = containerAsJar(this.getCompilationUnit().getParentContainer*()) }

/** Gets a node that is an input to a call to this API. */
private DataFlow::Node getAnInput() {
exists(Call call | call.getCallee().getSourceDeclaration() = this |
result.asExpr().(Argument).getCall() = call or
result.(ArgumentNode).getCall().asCall() = call
)
}

/** Gets a node that is an output from a call to this API. */
private DataFlow::Node getAnOutput() {
exists(Call call | call.getCallee().getSourceDeclaration() = this |
result.asExpr() = call or
result.(DataFlow::PostUpdateNode).getPreUpdateNode().(ArgumentNode).getCall().asCall() = call
)
}

/** Holds if this API has a supported summary. */
pragma[nomagic]
predicate hasSummary() {
this = any(SummarizedCallable sc).asCallable() or
TaintTracking::localAdditionalTaintStep(this.getAnInput(), _)
}

pragma[nomagic]
predicate isSource() {
this.getAnOutput() instanceof RemoteFlowSource or sourceNode(this.getAnOutput(), _)
}

/** Holds if this API is a known sink. */
pragma[nomagic]
predicate isSink() { sinkNode(this.getAnInput(), _) }

/** 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. */
predicate isSupported() { this.hasSummary() or this.isSource() or this.isSink() }
}

/** DEPRECATED: Alias for ExternalApi */
deprecated class ExternalAPI = ExternalApi;

/**
* Gets the limit for the number of results produced by a telemetry query.
*/
int resultLimit() { result = 1000 }

/**
* Holds if it is relevant to count usages of \`api\`.
*/
signature predicate relevantApi(ExternalApi api);

/**
* Given a predicate to count relevant API usages, this module provides a predicate
* for restricting the number or returned results based on a certain limit.
*/
module Results<relevantApi/1 getRelevantUsages> {
private int getUsages(string apiName) {
result =
strictcount(Call c, ExternalApi api |
c.getCallee().getSourceDeclaration() = api and
not c.getFile() instanceof GeneratedFile and
apiName = api.getApiName() and
getRelevantUsages(api)
)
}

private int getOrder(string apiInfo) {
apiInfo =
rank[result](string info, int usages |
usages = getUsages(info)
|
info order by usages desc, info
)
}

/**
* Holds if there exists an API with \`apiName\` that is being used \`usages\` times
* and if it is in the top results (guarded by resultLimit).
*/
predicate restrict(string apiName, int usages) {
usages = getUsages(apiName) and
getOrder(apiName) <= resultLimit()
}
}
`,
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type Query = {
mainQuery: string;
dependencies?: {
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.

I wonder if we should just inline ExternalApi.qll, so that we just have a single query file that we run instead of having to store multiple files?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think it would be easier to transfer these queries to the codeql repository if we leave them as separate files. So far, we've not had to make any modifications to the ExternalApi.qll file, so we would only need to add in the main query and not make any changes to the ExternalApi.qll file. If we do combine these files, we'd need to split them out again in the future.

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.

I dont think it would be too hard to do that split up later, but I dont feel strongly. It also doesnt seem super complicated to have extra dependency files because we also need to have the synthetic pack. So happy to leave it like this.

[filename: string]: string;
};
};
Loading