mirror of
https://github.com/github/codeql.git
synced 2026-04-30 19:26:02 +02:00
Merge pull request #8254 from asgerf/ruby/mad-prototype
Ruby: initial prototype of models-as-data
This commit is contained in:
@@ -371,8 +371,12 @@ module API {
|
||||
/**
|
||||
* An API entry point.
|
||||
*
|
||||
* Extend this class to define additional API entry points other than modules.
|
||||
* Typical examples include global variables.
|
||||
* By default, API graph nodes are only created for nodes that come from an external
|
||||
* library or escape into an external library. The points where values are cross the boundary
|
||||
* between codebases are called "entry points".
|
||||
*
|
||||
* Imports and exports are considered entry points by default, but additional entry points may
|
||||
* be added by extending this class. Typical examples include global variables.
|
||||
*/
|
||||
abstract class EntryPoint extends string {
|
||||
bindingset[this]
|
||||
@@ -385,7 +389,10 @@ module API {
|
||||
abstract DataFlow::Node getARhs();
|
||||
|
||||
/** Gets an API-node for this entry point. */
|
||||
API::Node getNode() { result = root().getASuccessor(Label::entryPoint(this)) }
|
||||
API::Node getANode() { result = root().getASuccessor(Label::entryPoint(this)) }
|
||||
|
||||
/** DEPRECATED. Use `getANode()` instead. */
|
||||
deprecated API::Node getNode() { result = this.getANode() }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,7 +23,7 @@ module D3 {
|
||||
or
|
||||
result = API::moduleImport("d3-node").getInstance().getMember("d3")
|
||||
or
|
||||
result = any(D3GlobalEntry i).getNode()
|
||||
result = any(D3GlobalEntry i).getANode()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,7 @@ module History {
|
||||
* Gets a reference to the [`history`](https://npmjs.org/package/history) library.
|
||||
*/
|
||||
private API::Node history() {
|
||||
result = [API::moduleImport("history"), any(HistoryGlobalEntry h).getNode()]
|
||||
result = [API::moduleImport("history"), any(HistoryGlobalEntry h).getANode()]
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,7 +27,7 @@ private module Immutable {
|
||||
API::Node immutableImport() {
|
||||
result = API::moduleImport("immutable")
|
||||
or
|
||||
result = any(ImmutableGlobalEntry i).getNode()
|
||||
result = any(ImmutableGlobalEntry i).getANode()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,7 +45,7 @@ private module Console {
|
||||
*/
|
||||
private API::Node console() {
|
||||
result = API::moduleImport("console") or
|
||||
result = any(ConsoleGlobalEntry e).getNode()
|
||||
result = any(ConsoleGlobalEntry e).getANode()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -151,7 +151,7 @@ module NestJS {
|
||||
private API::Node validationPipe() {
|
||||
result = nestjs().getMember("ValidationPipe")
|
||||
or
|
||||
result = any(ValidationNodeEntry e).getNode()
|
||||
result = any(ValidationNodeEntry e).getANode()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1111,7 +1111,7 @@ module Redux {
|
||||
|
||||
/** A heuristic call to `connect`, recognized by it taking arguments named `mapStateToProps` and `mapDispatchToProps`. */
|
||||
private class HeuristicConnectFunction extends ConnectCall {
|
||||
HeuristicConnectFunction() { this = any(HeuristicConnectEntryPoint e).getNode().getACall() }
|
||||
HeuristicConnectFunction() { this = any(HeuristicConnectEntryPoint e).getANode().getACall() }
|
||||
|
||||
override API::Node getMapStateToProps() {
|
||||
result = getAParameter() and
|
||||
|
||||
@@ -19,7 +19,7 @@ module TrustedTypes {
|
||||
override DataFlow::Node getARhs() { none() }
|
||||
}
|
||||
|
||||
private API::Node trustedTypesObj() { result = any(TrustedTypesEntry entry).getNode() }
|
||||
private API::Node trustedTypesObj() { result = any(TrustedTypesEntry entry).getANode() }
|
||||
|
||||
/** A call to `trustedTypes.createPolicy`. */
|
||||
class PolicyCreation extends API::CallNode {
|
||||
|
||||
@@ -35,7 +35,7 @@ module Vue {
|
||||
API::Node vueLibrary() {
|
||||
result = API::moduleImport("vue")
|
||||
or
|
||||
result = any(GlobalVueEntryPoint e).getNode()
|
||||
result = any(GlobalVueEntryPoint e).getANode()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,7 +51,7 @@ module Vue {
|
||||
or
|
||||
result = vueLibrary().getMember("component").getReturn()
|
||||
or
|
||||
result = any(VueFileImportEntryPoint e).getNode()
|
||||
result = any(VueFileImportEntryPoint e).getANode()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,10 +8,6 @@
|
||||
* The package name refers to an NPM package name or a path within a package name such as `lodash/extend`.
|
||||
* The string `global` refers to the global object (whether it came from the `global` package or not).
|
||||
*
|
||||
* The following tokens have a language-specific interpretation:
|
||||
* - `Instance`: the value returned by a `new`-call to a function
|
||||
* - `Awaited`: the value from a resolved promise
|
||||
*
|
||||
* A `(package, type)` tuple may refer to the exported type named `type` from the NPM package `package`.
|
||||
* For example, `(express, Request)` would match a parameter below due to the type annotation:
|
||||
* ```ts
|
||||
|
||||
@@ -22,16 +22,26 @@
|
||||
* or the empty string if referring to the package itself.
|
||||
* It can also be a synthetic type name defined by a type definition (see type definitions below).
|
||||
* 3. The `path` column is a `.`-separated list of "access path tokens" to resolve, starting at the node selected by `package` and `type`.
|
||||
* The possible access path tokens are:
|
||||
* - Member[x] : a property named `x`. May be a comma-separated list of named.
|
||||
*
|
||||
* Every language supports the following tokens:
|
||||
* - Argument[n]: the n-th argument to a call. May be a range of form `x..y` (inclusive) and/or a comma-separated list.
|
||||
* Additionally, `N-1` refers to the last argument, `N-2` refers to the second-last, and so on.
|
||||
* - Parameter[n]: the n-th parameter of a callback. May be a range of form `x..y` (inclusive) and/or a comma-separated list.
|
||||
* - ReturnValue: the value returned by a function call
|
||||
* - Instance: the value returned by a constructor call
|
||||
* - Awaited: the value from a resolved promise/future-like object
|
||||
* - WithArity[n]: match a call with the given arity. May be a range of form `x..y` (inclusive) and/or a comma-separated list.
|
||||
* - Other language-specific tokens mentioned in `ModelsAsData.qll`.
|
||||
*
|
||||
* The following tokens are common and should be implemented for languages where it makes sense:
|
||||
* - Member[x]: a member named `x`; exactly what a "member" is depends on the language. May be a comma-separated list of names.
|
||||
* - Instance: an instance of a class
|
||||
* - Subclass: a subclass of a class
|
||||
* - ArrayElement: an element of array
|
||||
* - Element: an element of a collection-like object
|
||||
* - MapKey: a key in map-like object
|
||||
* - MapValue: a value in a map-like object
|
||||
* - Awaited: the value from a resolved promise/future-like object
|
||||
*
|
||||
* For the time being, please consult `ApiGraphModelsSpecific.qll` to see which language-specific tokens are currently supported.
|
||||
*
|
||||
* 4. The `input` and `output` columns specify how data enters and leaves the element selected by the
|
||||
* first `(package, type, path)` tuple. Both strings are `.`-separated access paths
|
||||
* of the same syntax as the `path` column.
|
||||
@@ -149,6 +159,14 @@ module ModelInput {
|
||||
|
||||
private import ModelInput
|
||||
|
||||
/**
|
||||
* An empty class, except in specific tests.
|
||||
*
|
||||
* If this is non-empty, all models are parsed even if the package is not
|
||||
* considered relevant for the current database.
|
||||
*/
|
||||
abstract class TestAllModels extends Unit { }
|
||||
|
||||
/**
|
||||
* Append `;dummy` to the value of `s` to work around the fact that `string.split(delim,n)`
|
||||
* does not preserve empty trailing substrings.
|
||||
@@ -231,7 +249,17 @@ string getAPackageAlias(string package) {
|
||||
* Holds if CSV rows involving `package` might be relevant for the analysis of this database.
|
||||
*/
|
||||
private predicate isRelevantPackage(string package) {
|
||||
Specific::isPackageUsed(package)
|
||||
(
|
||||
sourceModel(package, _, _, _) or
|
||||
sinkModel(package, _, _, _) or
|
||||
summaryModel(package, _, _, _, _, _) or
|
||||
typeModel(package, _, _, _, _)
|
||||
) and
|
||||
(
|
||||
Specific::isPackageUsed(package)
|
||||
or
|
||||
exists(TestAllModels t)
|
||||
)
|
||||
or
|
||||
exists(string other |
|
||||
isRelevantPackage(other) and
|
||||
@@ -315,7 +343,7 @@ private predicate invocationMatchesCallSiteFilter(Specific::InvokeNode invoke, A
|
||||
* Gets the API node identified by the first `n` tokens of `path` in the given `(package, type, path)` tuple.
|
||||
*/
|
||||
pragma[nomagic]
|
||||
API::Node getNodeFromPath(string package, string type, AccessPath path, int n) {
|
||||
private API::Node getNodeFromPath(string package, string type, AccessPath path, int n) {
|
||||
isRelevantFullPath(package, type, path) and
|
||||
(
|
||||
n = 0 and
|
||||
@@ -357,6 +385,42 @@ Specific::InvokeNode getInvocationFromPath(string package, string type, AccessPa
|
||||
result = getInvocationFromPath(package, type, path, path.getNumToken())
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `name` is a valid name for an access path token in the identifying access path.
|
||||
*/
|
||||
bindingset[name]
|
||||
predicate isValidTokenNameInIdentifyingAccessPath(string name) {
|
||||
name = ["Argument", "Parameter", "ReturnValue", "WithArity"]
|
||||
or
|
||||
Specific::isExtraValidTokenNameInIdentifyingAccessPath(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `name` is a valid name for an access path token with no arguments, occuring
|
||||
* in an identifying access path.
|
||||
*/
|
||||
bindingset[name]
|
||||
predicate isValidNoArgumentTokenInIdentifyingAccessPath(string name) {
|
||||
name = "ReturnValue"
|
||||
or
|
||||
Specific::isExtraValidNoArgumentTokenInIdentifyingAccessPath(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `argument` is a valid argument to an access path token with the given `name`, occurring
|
||||
* in an identifying access path.
|
||||
*/
|
||||
bindingset[name, argument]
|
||||
predicate isValidTokenArgumentInIdentifyingAccessPath(string name, string argument) {
|
||||
name = ["Argument", "Parameter"] and
|
||||
argument.regexpMatch("(N-|-)?\\d+(\\.\\.(N-|-)?\\d+)?")
|
||||
or
|
||||
name = "WithArity" and
|
||||
argument.regexpMatch("\\d+(\\.\\.\\d+)?")
|
||||
or
|
||||
Specific::isExtraValidTokenArgumentInIdentifyingAccessPath(name, argument)
|
||||
}
|
||||
|
||||
/**
|
||||
* Module providing access to the imported models in terms of API graph nodes.
|
||||
*/
|
||||
@@ -382,26 +446,23 @@ module ModelOutput {
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a relevant CSV summary row has the given `kind`, `input` and `output`.
|
||||
* Holds if a relevant CSV summary exists for these parameters.
|
||||
*/
|
||||
predicate summaryModel(string input, string output, string kind) {
|
||||
exists(string package |
|
||||
isRelevantPackage(package) and
|
||||
summaryModel(package, _, _, input, output, kind)
|
||||
)
|
||||
predicate relevantSummaryModel(
|
||||
string package, string type, string path, string input, string output, string kind
|
||||
) {
|
||||
isRelevantPackage(package) and
|
||||
summaryModel(package, type, path, input, output, kind)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a summary edge with the given `input, output, kind` columns have a `package, type, path` tuple
|
||||
* that resolves to `baseNode`.
|
||||
* Holds if a `baseNode` is an invocation identified by the `package,type,path` part of a summary row.
|
||||
*/
|
||||
predicate resolvedSummaryBase(
|
||||
Specific::InvokeNode baseNode, AccessPath input, AccessPath output, string kind
|
||||
string package, string type, string path, Specific::InvokeNode baseNode
|
||||
) {
|
||||
exists(string package, string type, AccessPath path |
|
||||
summaryModel(package, type, path, input, output, kind) and
|
||||
baseNode = getInvocationFromPath(package, type, path)
|
||||
)
|
||||
summaryModel(package, type, path, _, _, _) and
|
||||
baseNode = getInvocationFromPath(package, type, path)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -414,4 +475,48 @@ module ModelOutput {
|
||||
result = getNodeFromPath(package2, type2, path)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an error message relating to an invalid CSV row in a model.
|
||||
*/
|
||||
string getAWarning() {
|
||||
// Check number of columns
|
||||
exists(string row, string kind, int expectedArity, int actualArity |
|
||||
any(SourceModelCsv csv).row(row) and kind = "source" and expectedArity = 4
|
||||
or
|
||||
any(SinkModelCsv csv).row(row) and kind = "sink" and expectedArity = 4
|
||||
or
|
||||
any(SummaryModelCsv csv).row(row) and kind = "summary" and expectedArity = 6
|
||||
or
|
||||
any(TypeModelCsv csv).row(row) and kind = "type" and expectedArity = 5
|
||||
|
|
||||
actualArity = count(row.indexOf(";")) + 1 and
|
||||
actualArity != expectedArity and
|
||||
result =
|
||||
"CSV " + kind + " row should have " + expectedArity + " columns but has " + actualArity +
|
||||
": " + row
|
||||
)
|
||||
or
|
||||
// Check names and arguments of access path tokens
|
||||
exists(AccessPath path, AccessPathToken token |
|
||||
isRelevantFullPath(_, _, path) and
|
||||
token = path.getToken(_)
|
||||
|
|
||||
not isValidTokenNameInIdentifyingAccessPath(token.getName()) and
|
||||
result = "Invalid token name '" + token.getName() + "' in access path: " + path
|
||||
or
|
||||
isValidTokenNameInIdentifyingAccessPath(token.getName()) and
|
||||
exists(string argument |
|
||||
argument = token.getAnArgument() and
|
||||
not isValidTokenArgumentInIdentifyingAccessPath(token.getName(), argument) and
|
||||
result =
|
||||
"Invalid argument '" + argument + "' in token '" + token + "' in access path: " + path
|
||||
)
|
||||
or
|
||||
isValidTokenNameInIdentifyingAccessPath(token.getName()) and
|
||||
token.getNumArgument() = 0 and
|
||||
not isValidNoArgumentTokenInIdentifyingAccessPath(token.getName()) and
|
||||
result = "Invalid token '" + token + "' is missing its arguments, in access path: " + path
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Contains the language-specific part of the models-as-data implementation found in `ApiGraphModels.qll`.
|
||||
*
|
||||
* It must export the following members:
|
||||
* ```codeql
|
||||
* ```ql
|
||||
* class Unit // a unit type
|
||||
* module API // the API graph module
|
||||
* predicate isPackageUsed(string package)
|
||||
@@ -19,6 +19,7 @@ private import ApiGraphModels
|
||||
|
||||
class Unit = JS::Unit;
|
||||
|
||||
// Re-export libraries needed by ApiGraphModels.qll
|
||||
module API = JS::API;
|
||||
|
||||
import semmle.javascript.frameworks.data.internal.AccessPathSyntax as AccessPathSyntax
|
||||
@@ -66,7 +67,7 @@ private class GlobalApiEntryPoint extends API::EntryPoint {
|
||||
* Gets an API node referring to the given global variable (if relevant).
|
||||
*/
|
||||
private API::Node getGlobalNode(string globalName) {
|
||||
result = any(GlobalApiEntryPoint e | e.getGlobal() = globalName).getNode()
|
||||
result = any(GlobalApiEntryPoint e | e.getGlobal() = globalName).getANode()
|
||||
}
|
||||
|
||||
/** Gets a JavaScript-specific interpretation of the `(package, type, path)` tuple after resolving the first `n` access path tokens. */
|
||||
@@ -147,10 +148,12 @@ predicate invocationMatchesExtraCallSiteFilter(API::InvokeNode invoke, AccessPat
|
||||
* Holds if `path` is an input or output spec for a summary with the given `base` node.
|
||||
*/
|
||||
pragma[nomagic]
|
||||
private predicate relevantInputOutputPath(API::InvokeNode base, AccessPath path) {
|
||||
ModelOutput::resolvedSummaryBase(base, path, _, _)
|
||||
or
|
||||
ModelOutput::resolvedSummaryBase(base, _, path, _)
|
||||
private predicate relevantInputOutputPath(API::InvokeNode base, AccessPath inputOrOutput) {
|
||||
exists(string package, string type, string input, string output, string path |
|
||||
ModelOutput::relevantSummaryModel(package, type, path, input, output, _) and
|
||||
ModelOutput::resolvedSummaryBase(package, type, path, base) and
|
||||
inputOrOutput = [input, output]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,8 +181,12 @@ private API::Node getNodeFromInputOutputPath(API::InvokeNode baseNode, AccessPat
|
||||
* Holds if a CSV summary contributed the step `pred -> succ` of the given `kind`.
|
||||
*/
|
||||
predicate summaryStep(API::Node pred, API::Node succ, string kind) {
|
||||
exists(API::InvokeNode base, AccessPath input, AccessPath output |
|
||||
ModelOutput::resolvedSummaryBase(base, input, output, kind) and
|
||||
exists(
|
||||
string package, string type, string path, API::InvokeNode base, AccessPath input,
|
||||
AccessPath output
|
||||
|
|
||||
ModelOutput::relevantSummaryModel(package, type, path, input, output, kind) and
|
||||
ModelOutput::resolvedSummaryBase(package, type, path, base) and
|
||||
pred = getNodeFromInputOutputPath(base, input) and
|
||||
succ = getNodeFromInputOutputPath(base, output)
|
||||
)
|
||||
@@ -189,3 +196,29 @@ class InvokeNode = API::InvokeNode;
|
||||
|
||||
/** Gets an `InvokeNode` corresponding to an invocation of `node`. */
|
||||
InvokeNode getAnInvocationOf(API::Node node) { result = node.getAnInvocation() }
|
||||
|
||||
/**
|
||||
* Holds if `name` is a valid name for an access path token in the identifying access path.
|
||||
*/
|
||||
bindingset[name]
|
||||
predicate isExtraValidTokenNameInIdentifyingAccessPath(string name) {
|
||||
name = ["Member", "Instance", "Awaited", "ArrayElement", "Element", "MapValue", "NewCall", "Call"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `name` is a valid name for an access path token with no arguments, occuring
|
||||
* in an identifying access path.
|
||||
*/
|
||||
predicate isExtraValidNoArgumentTokenInIdentifyingAccessPath(string name) {
|
||||
name = ["Instance", "Awaited", "ArrayElement", "Element", "MapValue", "NewCall", "Call"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `argument` is a valid argument to an access path token with the given `name`, occurring
|
||||
* in an identifying access path.
|
||||
*/
|
||||
bindingset[name, argument]
|
||||
predicate isExtraValidTokenArgumentInIdentifyingAccessPath(string name, string argument) {
|
||||
name = ["Member"] and
|
||||
exists(argument)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
| CSV type row should have 5 columns but has 2: test;TooFewColumns |
|
||||
| CSV type row should have 5 columns but has 8: test;TooManyColumns;;;Member[Foo].Instance;too;many;columns |
|
||||
| Invalid argument '0-1' in token 'Argument[0-1]' in access path: Method[foo].Argument[0-1] |
|
||||
| Invalid argument '0..' in token 'Argument[0..]' in access path: Argument[0..].Member[password] |
|
||||
| Invalid argument '0..' in token 'Argument[0..]' in access path: Argument[0..].Member[username] |
|
||||
| Invalid argument '0..' in token 'Argument[0..]' in access path: Member[executeSql].Argument[0..].Parameter[1] |
|
||||
| Invalid argument '0..' in token 'Argument[0..]' in access path: Member[run].Argument[0..].Parameter[1] |
|
||||
| Invalid argument '*' in token 'Argument[*]' in access path: Method[foo].Argument[*] |
|
||||
| Invalid token 'Argument' is missing its arguments, in access path: Method[foo].Argument |
|
||||
| Invalid token 'Member' is missing its arguments, in access path: Method[foo].Member |
|
||||
| Invalid token name 'Arg' in access path: Method[foo].Arg[0] |
|
||||
| Invalid token name 'Method' in access path: Method[foo].Arg[0] |
|
||||
| Invalid token name 'Method' in access path: Method[foo].Argument |
|
||||
| Invalid token name 'Method' in access path: Method[foo].Argument[0-1] |
|
||||
| Invalid token name 'Method' in access path: Method[foo].Argument[*] |
|
||||
| Invalid token name 'Method' in access path: Method[foo].Member |
|
||||
24
javascript/ql/test/library-tests/frameworks/data/warnings.ql
Normal file
24
javascript/ql/test/library-tests/frameworks/data/warnings.ql
Normal file
@@ -0,0 +1,24 @@
|
||||
import javascript
|
||||
import semmle.javascript.frameworks.data.internal.AccessPathSyntax as AccessPathSyntax
|
||||
import semmle.javascript.frameworks.data.internal.ApiGraphModels as ApiGraphModels
|
||||
|
||||
private class InvalidTypeModel extends ModelInput::TypeModelCsv {
|
||||
override predicate row(string row) {
|
||||
row =
|
||||
[
|
||||
"test;TooManyColumns;;;Member[Foo].Instance;too;many;columns", //
|
||||
"test;TooFewColumns", //
|
||||
"test;X;test;Y;Method[foo].Arg[0]", //
|
||||
"test;X;test;Y;Method[foo].Argument[0-1]", //
|
||||
"test;X;test;Y;Method[foo].Argument[*]", //
|
||||
"test;X;test;Y;Method[foo].Argument", //
|
||||
"test;X;test;Y;Method[foo].Member", //
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class IsTesting extends ApiGraphModels::TestAllModels {
|
||||
IsTesting() { this = this }
|
||||
}
|
||||
|
||||
query predicate warning = ModelOutput::getAWarning/0;
|
||||
Reference in New Issue
Block a user