mirror of
https://github.com/github/codeql.git
synced 2026-04-26 17:25:19 +02:00
Merge pull request #14679 from hmac/hmac-model-editor-ruby
Ruby: Experimental model editor support
This commit is contained in:
@@ -57,7 +57,10 @@ module Gem {
|
||||
}
|
||||
|
||||
/** Gets the name of the gem */
|
||||
string getName() { result = this.getSpecProperty("name").getConstantValue().getString() }
|
||||
string getName() {
|
||||
result = this.getSpecProperty("name").getConstantValue().getString() or
|
||||
result = specCall.getArgument(0).getAValueReachingSink().getConstantValue().getString()
|
||||
}
|
||||
|
||||
/** Gets a path that is loaded when the gem is required */
|
||||
private string getARequirePath() {
|
||||
|
||||
@@ -4,6 +4,7 @@ private import codeql.ruby.Concepts
|
||||
private import codeql.ruby.Frameworks
|
||||
private import codeql.ruby.dataflow.RemoteFlowSources
|
||||
private import codeql.ruby.dataflow.BarrierGuards
|
||||
private import codeql.ruby.frameworks.data.internal.ApiGraphModels
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting
|
||||
@@ -156,4 +157,8 @@ module CodeInjection {
|
||||
|
||||
override FlowState::State getAState() { result instanceof FlowState::Full }
|
||||
}
|
||||
|
||||
private class ExternalCodeInjectionSink extends Sink {
|
||||
ExternalCodeInjectionSink() { this = ModelOutput::getASinkNode("code-injection").asSink() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ private import codeql.ruby.dataflow.RemoteFlowSources
|
||||
private import codeql.ruby.Concepts
|
||||
private import codeql.ruby.Frameworks
|
||||
private import codeql.ruby.ApiGraphs
|
||||
private import codeql.ruby.frameworks.data.internal.ApiGraphModels
|
||||
|
||||
module CommandInjection {
|
||||
/**
|
||||
@@ -52,4 +53,10 @@ module CommandInjection {
|
||||
this.(DataFlow::CallNode).getMethodName() = "shellescape"
|
||||
}
|
||||
}
|
||||
|
||||
private class ExternalCommandInjectionSink extends Sink {
|
||||
ExternalCommandInjectionSink() {
|
||||
this = ModelOutput::getASinkNode("command-injection").asSink()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import codeql.ruby.DataFlow
|
||||
import codeql.ruby.TaintTracking
|
||||
import codeql.ruby.dataflow.RemoteFlowSources
|
||||
import codeql.ruby.frameworks.Core
|
||||
private import codeql.ruby.frameworks.data.internal.ApiGraphModels
|
||||
|
||||
/**
|
||||
* A data flow source for user input used in log entries.
|
||||
@@ -50,6 +51,10 @@ class LoggingSink extends Sink {
|
||||
LoggingSink() { this = any(Logging logging).getAnInput() }
|
||||
}
|
||||
|
||||
private class ExternalLogInjectionSink extends Sink {
|
||||
ExternalLogInjectionSink() { this = ModelOutput::getASinkNode("log-injection").asSink() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `String#replace` that replaces `\n` is considered to sanitize the replaced string (reduce false positive).
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,7 @@ private import codeql.ruby.Concepts
|
||||
private import codeql.ruby.DataFlow
|
||||
private import codeql.ruby.dataflow.BarrierGuards
|
||||
private import codeql.ruby.dataflow.RemoteFlowSources
|
||||
private import codeql.ruby.frameworks.data.internal.ApiGraphModels
|
||||
|
||||
module PathInjection {
|
||||
/**
|
||||
@@ -52,4 +53,8 @@ module PathInjection {
|
||||
class StringConstArrayInclusionCallAsSanitizer extends Sanitizer,
|
||||
StringConstArrayInclusionCallBarrier
|
||||
{ }
|
||||
|
||||
private class ExternalPathInjectionSink extends Sink {
|
||||
ExternalPathInjectionSink() { this = ModelOutput::getASinkNode("path-injection").asSink() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ private import codeql.ruby.DataFlow
|
||||
private import codeql.ruby.dataflow.BarrierGuards
|
||||
private import codeql.ruby.dataflow.RemoteFlowSources
|
||||
private import codeql.ruby.ApiGraphs
|
||||
private import codeql.ruby.frameworks.data.internal.ApiGraphModels
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting SQL injection
|
||||
@@ -56,4 +57,8 @@ module SqlInjection {
|
||||
{ }
|
||||
|
||||
private class SqlSanitizationAsSanitizer extends Sanitizer, SqlSanitization { }
|
||||
|
||||
private class ExternalSqlInjectionSink extends Sink {
|
||||
ExternalSqlInjectionSink() { this = ModelOutput::getASinkNode("sql-injection").asSink() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ private import codeql.ruby.dataflow.RemoteFlowSources
|
||||
private import codeql.ruby.dataflow.BarrierGuards
|
||||
private import codeql.ruby.dataflow.Sanitizers
|
||||
private import codeql.ruby.frameworks.ActionController
|
||||
private import codeql.ruby.frameworks.data.internal.ApiGraphModels
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting
|
||||
@@ -73,6 +74,10 @@ module UrlRedirect {
|
||||
}
|
||||
}
|
||||
|
||||
private class ExternalUrlRedirectSink extends Sink {
|
||||
ExternalUrlRedirectSink() { this = ModelOutput::getASinkNode("url-redirection").asSink() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
*/
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/**
|
||||
* @name Generate flow models
|
||||
* @description Queries to generate source, sink, summary and type models.
|
||||
* @kind table
|
||||
* @id rb/utils/modeleditor/generate-model
|
||||
* @tags modeleditor generate-model framework-mode
|
||||
*/
|
||||
|
||||
private import internal.Types
|
||||
private import internal.Summaries
|
||||
|
||||
|
||||
17
ruby/ql/src/utils/modeleditor/ApplicationModeEndpoints.ql
Normal file
17
ruby/ql/src/utils/modeleditor/ApplicationModeEndpoints.ql
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @name Fetch endpoints for use in the model editor (application mode)
|
||||
* @description A list of 3rd party endpoints (methods and attributes) used in the codebase. Excludes test and generated code.
|
||||
* @kind table
|
||||
* @id rb/utils/modeleditor/application-mode-endpoints
|
||||
* @tags modeleditor endpoints application-mode
|
||||
*/
|
||||
|
||||
import codeql.ruby.AST
|
||||
|
||||
// This query is empty as Application Mode is not yet supported for Ruby.
|
||||
from
|
||||
Call usage, string package, string type, string name, string parameters, boolean supported,
|
||||
string namespace, string version, string supportedType, string classification
|
||||
where none()
|
||||
select usage, package, namespace, type, name, parameters, supported, namespace, version,
|
||||
supportedType, classification
|
||||
15
ruby/ql/src/utils/modeleditor/FrameworkModeEndpoints.ql
Normal file
15
ruby/ql/src/utils/modeleditor/FrameworkModeEndpoints.ql
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @name Fetch endpoints for use in the model editor (framework mode)
|
||||
* @description A list of endpoints accessible (methods and attributes) for consumers of the library. Excludes test and generated code.
|
||||
* @kind table
|
||||
* @id rb/utils/modeleditor/framework-mode-endpoints
|
||||
* @tags modeleditor endpoints framework-mode
|
||||
*/
|
||||
|
||||
import ruby
|
||||
import ModelEditor
|
||||
|
||||
from PublicEndpointFromSource endpoint
|
||||
select endpoint, endpoint.getNamespace(), endpoint.getTypeName(), endpoint.getName(),
|
||||
endpoint.getParameterTypes(), endpoint.getSupportedStatus(), endpoint.getFile().getBaseName(),
|
||||
endpoint.getSupportedType()
|
||||
173
ruby/ql/src/utils/modeleditor/ModelEditor.qll
Normal file
173
ruby/ql/src/utils/modeleditor/ModelEditor.qll
Normal file
@@ -0,0 +1,173 @@
|
||||
/** Provides classes and predicates related to handling APIs for the VS Code extension. */
|
||||
|
||||
private import ruby
|
||||
private import codeql.ruby.dataflow.FlowSummary
|
||||
private import codeql.ruby.dataflow.internal.DataFlowPrivate
|
||||
private import codeql.ruby.dataflow.internal.FlowSummaryImpl as FlowSummaryImpl
|
||||
private import codeql.ruby.dataflow.internal.FlowSummaryImplSpecific
|
||||
private import codeql.ruby.frameworks.core.Gem
|
||||
private import codeql.ruby.frameworks.data.ModelsAsData
|
||||
private import codeql.ruby.frameworks.data.internal.ApiGraphModelsExtensions
|
||||
private import queries.modeling.internal.Util as Util
|
||||
|
||||
/** Holds if the given callable is not worth supporting. */
|
||||
private predicate isUninteresting(DataFlow::MethodNode c) {
|
||||
c.getLocation().getFile() instanceof TestFile
|
||||
}
|
||||
|
||||
private predicate gemFileStep(Gem::GemSpec gem, Folder folder, int n) {
|
||||
n = 0 and folder.getAFile() = gem.(File)
|
||||
or
|
||||
exists(Folder parent, int m |
|
||||
gemFileStep(gem, parent, m) and
|
||||
parent.getAFolder() = folder and
|
||||
n = m + 1
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A callable method or accessor from either the Ruby Standard Library, a 3rd party library, or from the source.
|
||||
*/
|
||||
class Endpoint extends DataFlow::MethodNode {
|
||||
Endpoint() { this.isPublic() and not isUninteresting(this) }
|
||||
|
||||
File getFile() { result = this.getLocation().getFile() }
|
||||
|
||||
string getName() { result = this.getMethodName() }
|
||||
|
||||
/**
|
||||
* Gets the namespace of this endpoint.
|
||||
*/
|
||||
bindingset[this]
|
||||
string getNamespace() {
|
||||
exists(Folder folder | folder = this.getFile().getParentContainer() |
|
||||
// The nearest gemspec to this endpoint, if one exists
|
||||
result = min(Gem::GemSpec g, int n | gemFileStep(g, folder, n) | g order by n).getName()
|
||||
or
|
||||
not gemFileStep(_, folder, _) and
|
||||
result = ""
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the unbound type name of this endpoint.
|
||||
*/
|
||||
bindingset[this]
|
||||
string getTypeName() {
|
||||
result =
|
||||
any(DataFlow::ModuleNode m | m.getOwnInstanceMethod(this.getMethodName()) = this)
|
||||
.getQualifiedName() or
|
||||
result =
|
||||
any(DataFlow::ModuleNode m | m.getOwnSingletonMethod(this.getMethodName()) = this)
|
||||
.getQualifiedName() + "!"
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the parameter types of this endpoint.
|
||||
*/
|
||||
bindingset[this]
|
||||
string getParameterTypes() {
|
||||
// For now, return the names of postional and keyword parameters. We don't always have type information, so we can't return type names.
|
||||
// We don't yet handle splat params or block params.
|
||||
result =
|
||||
"(" +
|
||||
concat(string key, string value |
|
||||
value = any(int i | i.toString() = key | this.asCallable().getParameter(i)).getName()
|
||||
or
|
||||
exists(DataFlow::ParameterNode param |
|
||||
param = this.asCallable().getKeywordParameter(key)
|
||||
|
|
||||
value = key + ":"
|
||||
)
|
||||
|
|
||||
value, "," order by key
|
||||
) + ")"
|
||||
}
|
||||
|
||||
/** Holds if this API has a supported summary. */
|
||||
pragma[nomagic]
|
||||
predicate hasSummary() { none() }
|
||||
|
||||
/** Holds if this API is a known source. */
|
||||
pragma[nomagic]
|
||||
abstract predicate isSource();
|
||||
|
||||
/** Holds if this API is a known sink. */
|
||||
pragma[nomagic]
|
||||
abstract predicate isSink();
|
||||
|
||||
/** Holds if this API is a known neutral. */
|
||||
pragma[nomagic]
|
||||
predicate isNeutral() { none() }
|
||||
|
||||
/**
|
||||
* Holds if this API is supported by existing CodeQL libraries, that is, it is either a
|
||||
* recognized source, sink or neutral or it has a flow summary.
|
||||
*/
|
||||
predicate isSupported() {
|
||||
this.hasSummary() or this.isSource() or this.isSink() or this.isNeutral()
|
||||
}
|
||||
|
||||
boolean getSupportedStatus() { if this.isSupported() then result = true else result = false }
|
||||
|
||||
string getSupportedType() {
|
||||
this.isSink() and result = "sink"
|
||||
or
|
||||
this.isSource() and result = "source"
|
||||
or
|
||||
this.hasSummary() and result = "summary"
|
||||
or
|
||||
this.isNeutral() and result = "neutral"
|
||||
or
|
||||
not this.isSupported() and result = ""
|
||||
}
|
||||
}
|
||||
|
||||
string methodClassification(Call method) {
|
||||
method.getFile() instanceof TestFile and result = "test"
|
||||
or
|
||||
not method.getFile() instanceof TestFile and
|
||||
result = "source"
|
||||
}
|
||||
|
||||
class TestFile extends File {
|
||||
TestFile() {
|
||||
this.getRelativePath().regexpMatch(".*(test|spec).+") and
|
||||
not this.getAbsolutePath().matches("%/ql/test/%") // allows our test cases to work
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A callable where there exists a MaD sink model that applies to it.
|
||||
*/
|
||||
class SinkCallable extends DataFlow::MethodNode {
|
||||
SinkCallable() {
|
||||
exists(string type, string path, string method |
|
||||
method = path.regexpCapture("(Method\\[[^\\]]+\\]).*", 1) and
|
||||
Util::pathToMethod(this, type, method) and
|
||||
sinkModel(type, path, _)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A callable where there exists a MaD source model that applies to it.
|
||||
*/
|
||||
class SourceCallable extends DataFlow::CallableNode {
|
||||
SourceCallable() {
|
||||
exists(string type, string path, string method |
|
||||
method = path.regexpCapture("(Method\\[[^\\]]+\\]).*", 1) and
|
||||
Util::pathToMethod(this, type, method) and
|
||||
sourceModel(type, path, _)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A class of effectively public callables from source code.
|
||||
*/
|
||||
class PublicEndpointFromSource extends Endpoint {
|
||||
override predicate isSource() { this instanceof SourceCallable }
|
||||
|
||||
override predicate isSink() { this instanceof SinkCallable }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
| lib/module.rb:2:3:3:5 | foo | mylib | M1 | foo | (x,y) | false | module.rb | |
|
||||
| lib/module.rb:5:3:6:5 | self_foo | mylib | M1! | self_foo | (x,y) | false | module.rb | |
|
||||
| lib/mylib.rb:4:3:5:5 | foo | mylib | A | foo | (x,y,key1:) | false | mylib.rb | |
|
||||
| lib/mylib.rb:7:3:8:5 | bar | mylib | A | bar | (x) | false | mylib.rb | |
|
||||
| lib/mylib.rb:10:3:11:5 | self_foo | mylib | A! | self_foo | (x,y) | false | mylib.rb | |
|
||||
| lib/mylib.rb:19:5:20:7 | foo | mylib | A::ANested | foo | (x,y) | false | mylib.rb | |
|
||||
| lib/other.rb:6:3:7:5 | foo | mylib | B | foo | (x,y) | false | other.rb | |
|
||||
| other_lib/lib/other_gem.rb:3:9:4:11 | foo | other-lib | OtherLib::A | foo | (x,y) | false | other_gem.rb | |
|
||||
@@ -0,0 +1 @@
|
||||
utils/modeleditor/FrameworkModeEndpoints.ql
|
||||
7
ruby/ql/test/query-tests/utils/modeleditor/lib/module.rb
Normal file
7
ruby/ql/test/query-tests/utils/modeleditor/lib/module.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
module M1
|
||||
def foo(x, y)
|
||||
end
|
||||
|
||||
def self.self_foo(x, y)
|
||||
end
|
||||
end
|
||||
27
ruby/ql/test/query-tests/utils/modeleditor/lib/mylib.rb
Normal file
27
ruby/ql/test/query-tests/utils/modeleditor/lib/mylib.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
require_relative "./other"
|
||||
|
||||
class A
|
||||
def foo(x, y, key1:, **kwargs, &block)
|
||||
end
|
||||
|
||||
def bar(x, *args)
|
||||
end
|
||||
|
||||
def self.self_foo(x, y)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def private_1(x, y)
|
||||
end
|
||||
|
||||
class ANested
|
||||
def foo(x, y)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def private_2(x, y)
|
||||
end
|
||||
end
|
||||
end
|
||||
12
ruby/ql/test/query-tests/utils/modeleditor/lib/other.rb
Normal file
12
ruby/ql/test/query-tests/utils/modeleditor/lib/other.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
require_relative "./module"
|
||||
|
||||
class B
|
||||
include M1
|
||||
|
||||
def foo(x, y)
|
||||
end
|
||||
end
|
||||
|
||||
class C
|
||||
extend M1
|
||||
end
|
||||
3
ruby/ql/test/query-tests/utils/modeleditor/mylib.gemspec
Normal file
3
ruby/ql/test/query-tests/utils/modeleditor/mylib.gemspec
Normal file
@@ -0,0 +1,3 @@
|
||||
Gem::Specification.new do |s|
|
||||
s.name = "mylib"
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
module OtherLib
|
||||
class A
|
||||
def foo(x, y)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
Gem::Specification.new("other-lib")
|
||||
Reference in New Issue
Block a user