Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ruby: prototype instantiation of graph export #16165

Closed
wants to merge 4 commits into from
Closed
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
4 changes: 4 additions & 0 deletions config/identical-files.json
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,10 @@
"ruby/ql/lib/codeql/ruby/frameworks/data/internal/ApiGraphModelsExtensions.qll",
"python/ql/lib/semmle/python/frameworks/data/internal/ApiGraphModelsExtensions.qll"
],
"ApiGraphModelsExport": [
"javascript/ql/lib/semmle/javascript/frameworks/data/internal/ApiGraphModelsExport.qll",
"ruby/ql/lib/codeql/ruby/frameworks/data/internal/ApiGraphModelsExport.qll"
],
"Swift declarations test file": [
"swift/ql/test/extractor-tests/declarations/declarations.swift",
"swift/ql/test/library-tests/ast/declarations.swift"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ typeModel
| (aliases).Alias1.prototype | (aliases).Alias1 | Instance |
| (aliases).Alias1.prototype | (aliases).Alias1.prototype.foo | ReturnValue |
| (aliases).Alias1.prototype.foo | (aliases).Alias1.prototype | Member[foo] |
| (long-access-path).a.shortcut.d | (long-access-path) | Member[a].Member[b].Member[c].Member[d] |
| (long-access-path).a.shortcut.d | (long-access-path) | Member[a].Member[shortcut].Member[d] |
| (long-access-path).a.shortcut.d.e | (long-access-path).a.shortcut.d | Member[e] |
| (long-access-path).a.shortcut.d.e | (long-access-path) | Member[a].Member[b].Member[c].Member[d].Member[e] |
| (long-access-path).a.shortcut.d.e | (long-access-path) | Member[a].Member[shortcut].Member[d].Member[e] |
| (reexport).func | (reexport) | Member[func] |
| (return-this).FluentInterface | (return-this) | Member[FluentInterface] |
| (return-this).FluentInterface.prototype | (return-this).FluentInterface | Instance |
Expand Down
18 changes: 16 additions & 2 deletions ruby/ql/lib/codeql/ruby/ApiGraphs.qll
Original file line number Diff line number Diff line change
Expand Up @@ -1047,15 +1047,29 @@ module API {

import MkShared

/** Gets the API node corresponding to the module/class object for `mod`. */
/** Gets the API node corresponding to the module/class object for `mod`, with epsilon edges to descendent modules/classes. */
bindingset[mod]
pragma[inline_late]
Node getModuleNode(DataFlow::ModuleNode mod) { result = Impl::MkModuleObjectDown(mod) }

/** Gets the API node corresponding to instances of `mod`. */
/** Gets the API node corresponding to instances of `mod`, with epsilon edges to instances of descendent modules/classes. */
bindingset[mod]
pragma[inline_late]
Node getModuleInstance(DataFlow::ModuleNode mod) { result = getModuleNode(mod).getInstance() }

/** Gets the API node corresponding to instances of `mod` with epsilon edges to ancestor modules/classes. */
bindingset[mod]
pragma[inline_late]
Node getModuleNodeUp(DataFlow::ModuleNode mod) { result = Impl::MkModuleObjectUp(mod) }

/** Gets the API node corresponding to instances of `mod`, with epsilon edges to instances of ancestor modules/classes. */
bindingset[mod]
pragma[inline_late]
Node getModuleInstanceUp(DataFlow::ModuleNode mod) {
result = getModuleNodeUp(mod).getInstance()
}

import Impl
}

private import Internal
Expand Down
114 changes: 114 additions & 0 deletions ruby/ql/lib/codeql/ruby/frameworks/data/ModelsAsData.qll
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,117 @@
)
}
}

/**
* Specifies which parts of the API graph to export in `ModelExport`.
*/
signature module ModelExportSig {
/**
* Holds if the exported model should contain `node`, if it is publicly accessible.
*
* This ensures that all ways to access `node` will be exported in type models.
*/
predicate shouldContain(API::Node node);

/**
* Holds if a named must be generated for `node` if it is to be included in the exported graph.
*/
default predicate mustBeNamed(API::Node node) { none() }

/**
* Holds if the exported model should preserve all paths leading to an instance of `type`,
* including partial ones. It does not need to be closed transitively, `ModelExport` will
* extend this to include type models from which `type` can be derived.
*/
default predicate shouldContainType(string type) { none() }
}

/**
* Module for exporting type models for a given set of nodes in the API graph.
*/
module ModelExport<ModelExportSig S> {
private import codeql.mad.dynamic.GraphExport
private import internal.ApiGraphModelsExport

private module GraphExportConfig implements GraphExportSig<Location, API::Node> {
predicate edge = Specific::apiGraphHasEdge/3;

predicate shouldContain = S::shouldContain/1;

predicate shouldNotContain(API::Node node) {
// Only export def-nodes, exclude use-nodes
node instanceof API::Internal::MkModuleObjectDown
or
node instanceof API::Internal::MkModuleInstanceDown
or
node instanceof API::Internal::MkForwardNode
or
node instanceof API::Internal::MkMethodAccessNode
}

predicate mustBeNamed(API::Node node) { S::mustBeNamed(node) }

predicate exposedName(API::Node node, string type, string path) {
path = "" and
exists(DataFlow::ModuleNode mod |
node = API::Internal::MkModuleObjectUp(mod) and
type = mod.getQualifiedName() + "!"
or
node = API::Internal::MkModuleInstanceUp(mod) and
type = mod.getQualifiedName()
)
}

private string suggestedMethodName(DataFlow::MethodNode method) {

Check warning

Code scanning / CodeQL

Dead code Warning

This code is never used, and it's not publicly exported.
exists(DataFlow::ModuleNode mod, string name |
method = mod.getOwnSingletonMethod(name) and
result = mod.getQualifiedName() + "." + name
or
method = mod.getOwnInstanceMethod(name) and
result = mod.getQualifiedName() + "#" + name
)
}

predicate suggestedName(API::Node node, string type) {
// exists(DataFlow::MethodNode method |
// node.asSink() = method.getAReturnNode() and type = suggestedMethodName(method) + "()"
// )
none()
}

bindingset[host]
predicate hasTypeSummary(API::Node host, string path) {
exists(string methodName |
methodReturnsReceiver(host.getMethod(methodName).asCallable()) and
path = "Method[" + methodName + "].ReturnValue"
)
}

pragma[nomagic]
private predicate methodReturnsReceiver(DataFlow::MethodNode func) {
getAReceiverRef(func).flowsTo(func.getAReturnNode())
}

pragma[nomagic]
private DataFlow::CallNode getAReceiverCall(DataFlow::MethodNode func) {
result = getAReceiverRef(func).getAMethodCall()
}

pragma[nomagic]
private predicate callReturnsReceiver(DataFlow::CallNode call) {
methodReturnsReceiver(call.getATarget())
}

pragma[nomagic]
private DataFlow::LocalSourceNode getAReceiverRef(DataFlow::MethodNode func) {
result = func.getSelfParameter()
or
result = getAReceiverCall(func) and
callReturnsReceiver(result)
}
}

private module ExportedGraph = TypeGraphExport<GraphExportConfig, S::shouldContainType/1>;

import ExportedGraph
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* Contains an extension of `GraphExport` that relies on API graph specific functionality.
*/

private import ApiGraphModels as Shared
private import codeql.mad.dynamic.GraphExport
private import ApiGraphModelsSpecific as Specific

private module API = Specific::API;

private import Shared

/**
* Holds if some proper prefix of `(type, path)` evaluated to `node`, where `remainingPath`
* is bound to the suffix of `path` that was not evaluated yet.
*/
bindingset[type, path]
predicate partiallyEvaluatedModel(string type, string path, API::Node node, string remainingPath) {
exists(int n, AccessPath accessPath |
accessPath = path and
getNodeFromPath(type, accessPath, n) = node and
n > 0 and
// Note that `n < accessPath.getNumToken()` is implied by the use of strictconcat()
remainingPath =
strictconcat(int k |
k = [n .. accessPath.getNumToken() - 1]
|
accessPath.getToken(k), "." order by k
)
)
}

/**
* Holds if `type` and all types leading to `type` should be re-exported.
*/
signature predicate shouldContainTypeSig(string type);

/**
* Wrapper around `GraphExport` that also exports information about re-exported types.
*
* ### JavaScript example 1
* For example, suppose `shouldContainType("foo")` holds, and the following is the entry point for a package `bar`:
* ```js
* // bar.js
* module.exports.xxx = require('foo');
* ```
* then this would generate the following type model:
* ```
* foo; bar; Member[xxx]
* ```
*
* ### JavaScript example 2
* For a more complex case, suppose the following type model exists:
* ```
* foo.XYZ; foo; Member[x].Member[y].Member[z]
* ```
* And the package exports something that matches a prefix of the access path above:
* ```js
* module.exports.blah = require('foo').x.y;
* ```
* This would result in the following type model:
* ```
* foo.XYZ; bar; Member[blah].Member[z]
* ```
* Notice that the access path `Member[blah].Member[z]` consists of an access path generated from the API
* graph, with pieces of the access path from the original type model appended to it.
*/
module TypeGraphExport<
GraphExportSig<Specific::Location, API::Node> S, shouldContainTypeSig/1 shouldContainType>
{
/** Like `shouldContainType` but includes types that lead to `type` via type models. */
private predicate shouldContainTypeEx(string type) {
shouldContainType(type)
or
exists(string prevType |
shouldContainType(prevType) and
Shared::typeModel(prevType, type, _)
)
}

private module Config implements GraphExportSig<Specific::Location, API::Node> {
import S

predicate shouldContain(API::Node node) {
S::shouldContain(node)
or
exists(string type1 | shouldContainTypeEx(type1) |
ModelOutput::getATypeNode(type1).getAValueReachableFromSource() = node.asSink()
or
exists(string type2, string path |
Shared::typeModel(type1, type2, path) and
getNodeFromPath(type2, path, _).getAValueReachableFromSource() = node.asSink()
)
)
}
}

private module ExportedGraph = GraphExport<Specific::Location, API::Node, Config>;

import ExportedGraph

/**
* Holds if `type1, type2, path` should be emitted as a type model, that is `(type2, path)` leads to an instance of `type1`.
*/
predicate typeModel(string type1, string type2, string path) {
ExportedGraph::typeModel(type1, type2, path)
or
shouldContainTypeEx(type1) and
exists(API::Node node |
// A relevant type is exported directly
Specific::sourceFlowsToSink(ModelOutput::getATypeNode(type1), node) and
ExportedGraph::pathToNode(type2, path, node)
or
// Something that leads to a relevant type, but didn't finish its access path, is exported
exists(string midType, string midPath, string remainingPath, string prefix, API::Node source |
Shared::typeModel(type1, midType, midPath) and
partiallyEvaluatedModel(midType, midPath, source, remainingPath) and
Specific::sourceFlowsToSink(source, node) and
ExportedGraph::pathToNode(type2, prefix, node) and
path = join(prefix, remainingPath)
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import codeql.ruby.DataFlow::DataFlow as DataFlow
private import FlowSummaryImpl::Public
private import codeql.ruby.dataflow.internal.DataFlowDispatch as DataFlowDispatch
import codeql.Locations // re-export Location

pragma[nomagic]
private predicate isUsedTopLevelConstant(string name) {
Expand Down Expand Up @@ -248,3 +249,54 @@
}

module ModelOutputSpecific { }

/**
* Holds if the value of `source` is exposed at `sink`.
*/
bindingset[source]
predicate sourceFlowsToSink(API::Node source, API::Node sink) {
// TODO: also establish subclass relationship
source.getAValueReachableFromSource() = sink.asSink()
}

/**
* Holds if the edge `pred -> succ` labelled with `path` exists in the API graph.
*/
bindingset[pred]
predicate apiGraphHasEdge(API::Node pred, string path, API::Node succ) {
exists(string name |
API::Internal::methodEdge(pred, name, succ) and path = "Method[" + name + "]"
)
or
API::Internal::elementEdge(pred, succ) and path = "Element"
or
API::Internal::instanceEdge(pred, succ) and path = "Instance"
or
API::Internal::returnEdge(pred, succ) and path = "ReturnValue"
or
exists(DataFlowDispatch::ArgumentPosition pos |
not pos.isSelf() and
API::Internal::argumentEdge(pred, pos, succ) and
path = "Argument[" + FlowSummaryImpl::Input::encodeArgumentPosition(pos) + "]"
)
or
exists(DataFlowDispatch::ParameterPosition pos |
not pos.isSelf() and
API::Internal::parameterEdge(pred, pos, succ) and
path = "Parameter[" + FlowSummaryImpl::Input::encodeParameterPosition(pos) + "]"
)
or
path = "" and
API::Internal::epsilonEdge(pred, succ)
}

pragma[nomagic]
private predicate inheritanceEdge(API::Node pred, API::Node succ) {

Check warning

Code scanning / CodeQL

Dead code Warning

This code is never used, and it's not publicly exported.
exists(DataFlow::ModuleNode mod |
pred = API::Internal::getModuleNodeUp(mod) and
succ = API::Internal::getModuleNodeUp(mod.getAnImmediateAncestor())
or
pred = API::Internal::getModuleInstanceUp(mod) and
succ = API::Internal::getModuleInstanceUp(mod.getAnImmediateAncestor())
)
}
2 changes: 2 additions & 0 deletions ruby/ql/lib/codeql/ruby/typetracking/ApiGraphShared.qll
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ module ApiGraphShared<ApiGraphSharedSig S> {

private import Cached

predicate epsilonEdge = Cached::epsilonEdge/2;

/** Gets an API node corresponding to the end of forward-tracking to `localSource`. */
pragma[nomagic]
private ApiNode forwardEndNode(DataFlow::LocalSourceNode localSource) {
Expand Down
Loading
Loading