Skip to content

Commit

Permalink
docs(go): improve the Go library documentation and make minor changes…
Browse files Browse the repository at this point in the history
… in API names.
  • Loading branch information
plusvic committed Mar 19, 2024
1 parent 396666f commit 3d74a44
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 31 deletions.
56 changes: 55 additions & 1 deletion go/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,29 @@ import (
"unsafe"
)

// Compiler represent a YARA compiler.
type Compiler struct {
cCompiler *C.YRX_COMPILER
}

// NewCompiler creates a new compiler.
func NewCompiler() *Compiler {
c := &Compiler{}
C.yrx_compiler_create(&c.cCompiler)
runtime.SetFinalizer(c, (*Compiler).Destroy)
return c
}

// AddSource adds some YARA source code to be compiled.
//
// This function can be called multiple times.
//
// Example:
//
// c := NewCompiler()
// c.AddSource("rule foo { condition: true }")
// c.AddSource("rule bar { condition: true }")
//
func (c *Compiler) AddSource(src string) error {
cSrc := C.CString(src)
defer C.free(unsafe.Pointer(cSrc))
Expand All @@ -42,7 +54,12 @@ func (c *Compiler) AddSource(src string) error {
return nil
}

func (c *Compiler) AddUnsupportedModule(module string) {
// IgnoreModule tells the compiler to ignore the module with the given name.
//
// Any YARA rule using the module will be ignored, as well as rules that depends
// on some other rule that uses the module. The compiler will issue warnings
// about the ignored rules, but otherwise the compilation will succeed.
func (c *Compiler) IgnoreModule(module string) {
cModule := C.CString(module)
defer C.free(unsafe.Pointer(cModule))
result := C.yrx_compiler_add_unsupported_module(c.cCompiler, cModule)
Expand All @@ -52,6 +69,23 @@ func (c *Compiler) AddUnsupportedModule(module string) {
runtime.KeepAlive(c)
}

// NewNamespace creates a new namespace.
//
// Later calls to [Compiler.AddSource] will put the rules under the newly created
// namespace.
//
// Examples:
//
// c := NewCompiler()
// // Add some rule named "foo" under the default namespace
// c.AddSource("rule foo { condition: true }")
//
// // Create a new namespace named "bar"
// c.NewNamespace("bar")
//
// // It's ok to add another rule named "foo", as it is in a different
// // namespace than the previous one.
// c.AddSource("rule foo { condition: true }")
func (c *Compiler) NewNamespace(namespace string) {
cNamespace := C.CString(namespace)
defer C.free(unsafe.Pointer(cNamespace))
Expand All @@ -62,6 +96,15 @@ func (c *Compiler) NewNamespace(namespace string) {
runtime.KeepAlive(c)
}

// DefineGlobal defines a global variable and sets its initial value.
//
// Global variables must be defined before using [Compiler.AddSource]
// for adding any YARA source code that uses those variables. The variable
// will retain its initial value when the compiled [Rules] are used for
// scanning data, however each scanner can change the variable's initial
// value by calling [Scanner.SetGlobal].
//
// Valid value types are: int, int32, int64, bool, string, float32 and float64.
func (c *Compiler) DefineGlobal(ident string, value interface{}) error {
cIdent := C.CString(ident)
defer C.free(unsafe.Pointer(cIdent))
Expand All @@ -83,6 +126,8 @@ func (c *Compiler) DefineGlobal(ident string, value interface{}) error {
cValue := C.CString(v)
defer C.free(unsafe.Pointer(cValue))
ret = C.int(C.yrx_compiler_define_global_str(c.cCompiler, cIdent, cValue))
case float32:
ret = C.int(C.yrx_compiler_define_global_float(c.cCompiler, cIdent, C.double(v)))
case float64:
ret = C.int(C.yrx_compiler_define_global_float(c.cCompiler, cIdent, C.double(v)))
default:
Expand All @@ -98,13 +143,22 @@ func (c *Compiler) DefineGlobal(ident string, value interface{}) error {
return nil
}

// Build creates a [Rules] object containing a compiled version of all the
// YARA rules previously added to the compiler.
//
// Once this function is called the compiler is reset to its initial state,
// as if it was a newly created compiler.
func (c *Compiler) Build() *Rules {
r := &Rules{cRules: C.yrx_compiler_build(c.cCompiler)}
runtime.SetFinalizer(r, (*Rules).Destroy)
runtime.KeepAlive(c)
return r
}

// Destroy destroys the compiler.
//
// Calling Destroy is not required, but it's useful for explicitly freeing
// the memory used by the compiler.
func (c *Compiler) Destroy() {
if c.cCompiler != nil {
C.yrx_compiler_destroy(c.cCompiler)
Expand Down
4 changes: 2 additions & 2 deletions go/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func TestNamespaces(t *testing.T) {

func TestUnsupportedModules(t *testing.T) {
c := NewCompiler()
c.AddUnsupportedModule("unsupported_module")
c.IgnoreModule("unsupported_module")
c.NewNamespace("foo")
c.AddSource(`
import "unsupported_module"
Expand All @@ -47,7 +47,7 @@ func TestSerialization(t *testing.T) {
func TestVariables(t *testing.T) {
r, _ := Compile(
"rule test { condition: var == 1234 }",
GlobalVars(map[string]interface{}{"var": 1234}))
Globals(map[string]interface{}{"var": 1234}))

matchingRules, _ := NewScanner(r).Scan([]byte{})
assert.Len(t, matchingRules, 1)
Expand Down
75 changes: 75 additions & 0 deletions go/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package yara_x

import "fmt"

func Example_basic() {
// Compile some YARA rules.
rules, _ := Compile(`
rule foo {
strings:
$foo = "foo"
condition:
$foo
}
rule bar {
strings:
$bar = "bar"
condition:
$bar
}`)

// Use the compiled rules for scanning some data.
matchingRules, _ := rules.Scan([]byte("foobar"))

// Iterate over the matching rules.
for _, r := range matchingRules {
fmt.Printf("rule %s matched\n", r.Identifier())
}

// Output:
// rule foo matched
// rule bar matched
}

func Example_compilerAndScanner() {
// Create a new compiler.
compiler := NewCompiler()

// Add some rules to the compiler.
err := compiler.AddSource(`rule foo {
strings:
$foo = "foo"
condition:
$foo
}
rule bar {
strings:
$bar = "bar"
condition:
$bar
}`)

if err != nil {
panic(err)
}

// Get the compiled rules.
rules := compiler.Build()

// Pass the compiled rules to a scanner.
scanner := NewScanner(rules)

// Use the scanner for scanning some data.
matchingRules, _ := scanner.Scan([]byte("foobar"))

// Iterate over the matching rules.
for _, r := range matchingRules {
fmt.Printf("rule %s matched\n", r.Identifier())
}

// Output:
// rule foo matched
// rule bar matched
}
50 changes: 33 additions & 17 deletions go/main.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
// Package yara_x provides Go bindings to the YARA-X library.
//
// Use the static_link build tag for linking the YARA-X Rust library statically.
// Notice however that this only works if the Rust library itself links all
// its dependencies statically.
package yara_x

// #cgo CFLAGS: -I${SRCDIR}/../capi/include
Expand All @@ -16,11 +12,17 @@ import (
"unsafe"
)

// Option represent an option passed to Compile.
type Option func(c *Compiler) error
// A CompileOption represent an option passed to [Compile].
type CompileOption func(c *Compiler) error

// GlobalVars is an option for Compile that allows defining global variables.
func GlobalVars(vars map[string]interface{}) Option {
// Globals is an option for [Compile] that allows defining global variables.
//
// Keys in the map are variable names, and values are the initial value for
// each variable. The value associated to each variable can be modified at
// scan time with [Scanner.SetGlobal].
//
// Valid value types are: int, int32, int64, bool, string, float32 and float64.
func Globals(vars map[string]interface{}) CompileOption {
return func(c *Compiler) error {
for ident, value := range vars {
if err := c.DefineGlobal(ident, value); err != nil {
Expand All @@ -31,18 +33,20 @@ func GlobalVars(vars map[string]interface{}) Option {
}
}

// UnsupportedModule is an option for Compile that allows specifying an
// unsupported module. See Compiler.AddUnsupportedModule for details.
func UnsupportedModule(module string) Option {
// IgnoreModule is an option for [Compile] that allows ignoring a given module.
//
// This option can be passed multiple times with different module names.
// See [Compiler.IgnoreModule] for details.
func IgnoreModule(module string) CompileOption {
return func(c *Compiler) error {
c.AddUnsupportedModule(module)
c.IgnoreModule(module)
return nil
}
}

// Compile receives YARA source code and returns compiled Rules that can be
// Compile receives YARA source code and returns compiled [Rules] that can be
// used for scanning data.
func Compile(source string, opts ...Option) (*Rules, error) {
func Compile(src string, opts ...CompileOption) (*Rules, error) {
c := NewCompiler()

for _, opt := range opts {
Expand All @@ -51,12 +55,15 @@ func Compile(source string, opts ...Option) (*Rules, error) {
}
}

if err := c.AddSource(source); err != nil {
if err := c.AddSource(src); err != nil {
return nil, err
}
return c.Build(), nil
}

// Deserialize deserializes rules from a byte slice.
//
// The counterpart is [Rules.Serialize]
func Deserialize(data []byte) (*Rules, error) {
var ptr *C.uint8_t
if len(data) > 0 {
Expand All @@ -78,6 +85,15 @@ func Deserialize(data []byte) (*Rules, error) {
// Rules represents a set of compiled YARA rules.
type Rules struct{ cRules *C.YRX_RULES }

// Scan some data with the compiled rules.
//
// Returns a slice with the rules that matched.
func (r* Rules) Scan(data []byte) ([]*Rule, error) {
scanner := NewScanner(r)
return scanner.Scan(data)
}

// Serialize converts the compiled rules into a byte slice.
func (r *Rules) Serialize() ([]byte, error) {
var buf *C.YRX_BUFFER
runtime.LockOSThread()
Expand All @@ -90,7 +106,7 @@ func (r *Rules) Serialize() ([]byte, error) {
return C.GoBytes(unsafe.Pointer(buf.data), C.int(buf.length)), nil
}

// Destroy destroys the compiled YARA rules represented by Rules.
// Destroy destroys the compiled YARA rules represented by [Rules].
//
// Calling this method directly is not necessary, it will be invoked by the
// garbage collector when the rules are not used anymore.
Expand Down Expand Up @@ -191,7 +207,7 @@ type Pattern struct {
matches []Match
}

// Identifier returns the pattern's identifier (i.e: `$a`, `$foo`).
// Identifier returns the pattern's identifier (i.e: $a, $foo).
func (p *Pattern) Identifier() string {
return p.identifier
}
Expand Down
15 changes: 7 additions & 8 deletions go/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,14 @@ func NewScanner(r *Rules) *Scanner {
return s
}

// Timeout sets a timeout for scan operations.
// SetTimeout sets a timeout for scan operations.
//
// The Scan method will return a timeout error once the provided timeout
// The [Scanner.Scan] method will return a timeout error once the provided timeout
// duration has elapsed. The scanner will make every effort to stop promptly
// after the designated timeout duration. However, in some cases, particularly
// with rules containing only a few patterns, the scanner could potentially
// continue running for a longer period than the specified timeout.
func (s *Scanner) Timeout(timeout time.Duration) {
func (s *Scanner) SetTimeout(timeout time.Duration) {
C.yrx_scanner_timeout(s.cScanner, C.uint64_t(math.Ceil(timeout.Seconds())))
runtime.KeepAlive(s)
}
Expand All @@ -92,7 +92,7 @@ var ErrTimeout = errors.New("timeout")

// SetGlobal sets the value of a global variable.
//
// The variable must has been previously defined by calling Compiler.DefineGlobal
// The variable must has been previously defined by calling [Compiler.DefineGlobal]
// and the type it has during the definition must match the type of the new
// value.
//
Expand Down Expand Up @@ -152,10 +152,9 @@ func (s *Scanner) SetGlobal(ident string, value interface{}) error {
// Case 1) applies to certain modules lacking a main function, thus incapable of
// producing any output on their own. For such modules, you must set the output
// before scanning the associated data. Since the module's output typically varies
// with each scanned file, you need to call [yrx_scanner_set_module_output] prior
// to each invocation of [yrx_scanner_scan]. Once [yrx_scanner_scan] is executed,
// the module's output is consumed and will be empty unless set again before the
// subsequent call.
// with each scanned file, you need to call this function prior to each invocation
// of [Scanner.Scan]. Once [Scanner.Scan] is executed, the module's output is
// consumed and will be empty unless set again before the subsequent call.
//
// Case 2) applies when you have previously stored the module's output for certain
// scanned data. In such cases, when rescanning the data, you can utilize this
Expand Down
6 changes: 3 additions & 3 deletions go/scanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func TestScanner2(t *testing.T) {
func TestScanner3(t *testing.T) {
r, _ := Compile(
`rule t { condition: var_bool }`,
GlobalVars(map[string]interface{}{"var_bool": true}))
Globals(map[string]interface{}{"var_bool": true}))

s := NewScanner(r)
matchingRules, _ := s.Scan([]byte{})
Expand All @@ -54,7 +54,7 @@ func TestScanner3(t *testing.T) {
func TestScanner4(t *testing.T) {
r, _ := Compile(
`rule t { condition: var_int == 1}`,
GlobalVars(map[string]interface{}{"var_int": 0}))
Globals(map[string]interface{}{"var_int": 0}))

s := NewScanner(r)
matchingRules, _ := s.Scan([]byte{})
Expand All @@ -76,7 +76,7 @@ func TestScanner4(t *testing.T) {
func TestScannerTimeout(t *testing.T) {
r, _ := Compile("rule t { strings: $a = /a(.*)*a/ condition: $a }")
s := NewScanner(r)
s.Timeout(1*time.Second)
s.SetTimeout(1*time.Second)
_, err := s.Scan(bytes.Repeat([]byte("a"), 9000))
assert.ErrorIs(t, err, ErrTimeout)
}

0 comments on commit 3d74a44

Please sign in to comment.