vt

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 2, 2024 License: Apache-2.0 Imports: 18 Imported by: 21

README

GoDoc Go Report Card

vt-go

This is the official Go client library for VirusTotal. With this library you can interact with the VirusTotal REST API v3 without having to send plain HTTP requests with the standard "http" package.

Usage example

package main

import (
	"flag"
	"fmt"
	"log"
	"os"

	vt "github.com/VirusTotal/vt-go"
)

var apikey = flag.String("apikey", "", "VirusTotal API key")
var sha256 = flag.String("sha256", "", "SHA-256 of some file")

func main() {

	flag.Parse()

	if *apikey == "" || *sha256 == "" {
		fmt.Println("Must pass both the --apikey and --sha256 arguments.")
		os.Exit(0)
	}

	client := vt.NewClient(*apikey)

	file, err := client.GetObject(vt.URL("files/%s", *sha256))
	if err != nil {
		log.Fatal(err)
	}

	ls, err := file.GetTime("last_submission_date")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("File %s was submitted for the last time on %v\n", file.ID(), ls)
}

Documentation

Overview

Package vt is a client library for the VirusTotal API v3. It makes the use of the VirusTotal's REST API easier for Go developers.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func SetHost

func SetHost(host string)

SetHost allows to change the host used while sending requests to the VirusTotal API. The default host is "www.virustotal.com" you rarely need to change it.

func URL

func URL(pathFmt string, a ...interface{}) *url.URL

URL returns a full VirusTotal API URL from a relative path (i.e: a path without the domain name and the "/api/v3/" prefix). The path can contain format 'verbs' as defined in the "fmt". This function is useful for creating URLs to be passed to any function expecting a *url.URL in this library.

Example
SetHost("https://www.virustotal.com")
url := URL("files/275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f")
fmt.Println(url)
url = URL("intelligence/retrohunt_jobs/%s", "1234567")
fmt.Println(url)
Output:

https://www.virustotal.com/api/v3/files/275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f
https://www.virustotal.com/api/v3/intelligence/retrohunt_jobs/1234567

Types

type Client

type Client struct {
	// APIKey is the VirusTotal API key that identifies the user making the
	// requests.
	APIKey string
	// Agent is a string included in the User-Agent header of every request
	// sent to VirusTotal's servers. Users of this client are encouraged to
	// use some string that uniquely identify the program making the requests.
	Agent string
	// contains filtered or unexported fields
}

Client for interacting with VirusTotal API.

func NewClient

func NewClient(APIKey string, opts ...ClientOption) *Client

NewClient creates a new client for interacting with the VirusTotal API using the provided API key.

func (*Client) Delete

func (cli *Client) Delete(url *url.URL, options ...RequestOption) (*Response, error)

Delete sends a DELETE request to the specified API endpoint.

func (*Client) DeleteData

func (cli *Client) DeleteData(url *url.URL, data interface{}, options ...RequestOption) (*Response, error)

DeleteData sends a DELETE request to the specified API endpoint. The data argument is JSON-encoded and wrapped as {'data': <JSON-encoded data>}.

func (*Client) DownloadFile

func (cli *Client) DownloadFile(hash string, w io.Writer) (int64, error)

DownloadFile downloads a file given its hash (SHA-256, SHA-1 or MD5). The file is written into the provided io.Writer.

func (*Client) Get

func (cli *Client) Get(url *url.URL, options ...RequestOption) (*Response, error)

Get sends a GET request to the specified API endpoint. This is a low level primitive that returns a Response struct, where the response's data is in raw form. See GetObject and GetData for higher level primitives.

func (*Client) GetData

func (cli *Client) GetData(url *url.URL, target interface{}, options ...RequestOption) (*Response, error)

GetData sends a GET request to the specified API endpoint and unmarshals the JSON-encoded data received in the API response. The unmarshalled data is put into the specified target. The target must be of an appropriate type capable of receiving the data returned by the the endpoint. If the data returned by the endpoint is an object you can use GetObject instead.

func (*Client) GetMetadata

func (cli *Client) GetMetadata() (*Metadata, error)

GetMetadata retrieves VirusTotal metadata by calling the /api/v3/metadata endpoint.

func (*Client) GetObject

func (cli *Client) GetObject(url *url.URL, options ...RequestOption) (*Object, error)

GetObject returns an Object from a URL. The specified URL must reference an object, not a collection. This means that GetObject can be used with URLs like /files/{file_id} and /urls/{url_id}, which return an individual object but not with /comments, which returns a collection of objects.

func (*Client) Iterator

func (cli *Client) Iterator(url *url.URL, options ...IteratorOption) (*Iterator, error)

Iterator returns an iterator for a collection. If the endpoint passed to the iterator returns a single object instead of a collection, it behaves as if iterating over a collection with a single object. Iterators are usually used like this:

 cli := vt.Client(<api key>)
 it, err := cli.Iterator(vt.URL(<collection path>))
 if err != nil {
	  ...handle error
 }
 defer it.Close()
 for it.Next() {
   obj := it.Get()
   ...do something with obj
 }
 if err := it.Error(); err != nil {
   ...handle error
 }

func (*Client) NewFeed

func (cli *Client) NewFeed(t FeedType, options ...FeedOption) (*Feed, error)

NewFeed creates a Feed that receives objects from the specified type. Objects are send on channel C. The feed can be stopped at any moment by calling Stop. This example illustrates how a Feed is typically used:

feed, err := vt.Client(<api key>).NewFeed(vt.FileFeed)
if err != nil {
   ... handle error
}
for fileObj := range feed.C {
   ... do something with file object
}
if feed.Error() != nil {
   ... feed as been stopped by some error.
}

func (*Client) NewFileScanner

func (cli *Client) NewFileScanner() *FileScanner

NewFileScanner returns a new FileScanner.

func (*Client) NewMonitorUploader

func (cli *Client) NewMonitorUploader() *MonitorUploader

NewMonitorUploader returns a new MonitorUploader.

func (*Client) NewURLScanner

func (cli *Client) NewURLScanner() *URLScanner

NewURLScanner returns a new URLScanner.

func (*Client) Patch

func (cli *Client) Patch(url *url.URL, req *Request, options ...RequestOption) (*Response, error)

Patch sends a PATCH request to the specified API endpoint.

func (*Client) PatchObject

func (cli *Client) PatchObject(url *url.URL, obj *Object, options ...RequestOption) error

PatchObject modifies an existing object.

func (*Client) Post

func (cli *Client) Post(url *url.URL, req *Request, options ...RequestOption) (*Response, error)

Post sends a POST request to the specified API endpoint.

func (*Client) PostData

func (cli *Client) PostData(url *url.URL, data interface{}, options ...RequestOption) (*Response, error)

PostData sends a POST request to the specified API endpoint. The data argument is JSON-encoded and wrapped as {'data': <JSON-encoded data> }.

func (*Client) PostObject

func (cli *Client) PostObject(url *url.URL, obj *Object, options ...RequestOption) error

PostObject adds an Object to a collection. The specified URL must point to a collection, not an object, but not all collections accept this operation. For more information about collection and objects in the VirusTotal API see:

https://developers.virustotal.com/v3.0/reference#objects https://developers.virustotal.com/v3.0/reference#collections

This function updates the object with data returned from the server, so the object's attributes can differ from those it had before the call.

Example:

obj := vt.NewObject("hunting_ruleset")
obj.SetString("name", "test")
obj.SetString("rules", "rule test {condition: false}")

client.PostObject(vt.URL("intelligence/hunting_rulesets"), obj)

func (*Client) Search

func (cli *Client) Search(query string, options ...IteratorOption) (*Iterator, error)

Search for files using VirusTotal Intelligence query language. Example:

it, err := client.Search("p:10+ size:30MB+")

type ClientOption

type ClientOption func(*Client)

ClientOption represents an option passed to NewClient.

func WithGlobalHeader

func WithGlobalHeader(header, value string) ClientOption

WithGlobalHeader specifies a global header to be included in the all the requests.

func WithHTTPClient

func WithHTTPClient(httpClient *http.Client) ClientOption

WithHTTPClient allows to set the http.Client used by Client. If not specified a default http.Client is used.

type Error

type Error struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

Error contains information about an API error.

func (Error) Error

func (e Error) Error() string

Error implements the error interface.

type Feed

type Feed struct {
	C chan *Object
	// contains filtered or unexported fields
}

A Feed represents a stream of objects received from VirusTotal via the feed API v3. This API allows you to get information about objects as they are processed by VirusTotal in real-time. Objects are sent on channel C.

func (*Feed) Cursor

func (f *Feed) Cursor() string

Cursor returns a string that can be passed to FeedCursor for creating a feed that resumes where a previous one left.

func (*Feed) Error

func (f *Feed) Error() error

Error returns any error occurred so far.

func (*Feed) Stop

func (f *Feed) Stop() error

Stop causes the feed to stop sending objects to the channel C. After Stop is called the feed still sends all the objects that it has buffered.

type FeedOption

type FeedOption func(*Feed) error

FeedOption represents an option passed to a NewFeed.

func FeedBufferSize

func FeedBufferSize(size int) FeedOption

FeedBufferSize specifies the size of the Feed's buffer.

func FeedCursor

func FeedCursor(cursor string) FeedOption

FeedCursor specifies the point in time where the feed starts. Files processed by VirusTotal after that time will be retrieved. The cursor is a string with the format YYYYMMDDhhmm, indicating the date and time with minute precision. If a empty string is passed as cursor the current time will be used.

type FeedType

type FeedType string

FeedType ...

const (
	// FileFeed is the feed type passed to NewFeed() for getting a feed with
	// all the files being scanned by VirusTotal.
	FileFeed FeedType = "files"
)

type FileScanner

type FileScanner struct {
	// contains filtered or unexported fields
}

FileScanner represents a file scanner.

func (*FileScanner) Scan

func (s *FileScanner) Scan(r io.Reader, filename string, progress chan<- float32) (*Object, error)

Scan sends a file to VirusTotal for scanning. The file content is read from the r io.Reader and sent to VirusTotal with the provided file name which can be left blank. The function also sends a float32 through the progress channel indicating the percentage of the file that has been already uploaded. The progress channel can be nil if the caller is not interested in receiving upload progress updates. An analysis object is returned as soon as the file is uploaded.

func (*FileScanner) ScanFile

func (s *FileScanner) ScanFile(f *os.File, progress chan<- float32) (*Object, error)

ScanFile sends a file to VirusTotal for scanning. This function is similar to Scan but it receive an *os.File instead of a io.Reader and a file name.

func (*FileScanner) ScanFileWithParameters

func (s *FileScanner) ScanFileWithParameters(
	f *os.File, progress chan<- float32, parameters map[string]string) (*Object, error)

ScanFileWithParameters sends a file to VirusTotal for scanning. This function is similar to ScanWithParameters but it receives an *os.File instead of a io.Reader and a file name.

func (*FileScanner) ScanParameters

func (s *FileScanner) ScanParameters(
	r io.Reader, filename string, progress chan<- float32, parameters map[string]string) (*Object, error)

ScanParameters sends a file to VirusTotal for scanning. The file content is read from the r io.Reader and sent to VirusTotal with the provided file name which can be left blank. The function also sends a float32 through the progress channel indicating the percentage of the file that has been already uploaded. The progress channel can be nil if the caller is not interested in receiving upload progress updates. An analysis object is returned as soon as the file is uploaded. Additional parameters can be passed to the scan by using the parameters map[string]string argument.

type Iterator

type Iterator struct {
	// contains filtered or unexported fields
}

Iterator represents a iterator over a collection of VirusTotal objects.

func (*Iterator) Close

func (it *Iterator) Close()

Close closes a collection iterator.

func (*Iterator) Cursor

func (it *Iterator) Cursor() string

Cursor returns a token indicating the current iterator's position.

func (*Iterator) Error

func (it *Iterator) Error() error

Error returns any error occurred during the iteration of a collection.

func (*Iterator) Get

func (it *Iterator) Get() *Object

Get returns the current object in the collection iterator.

func (*Iterator) Meta

func (it *Iterator) Meta() map[string]interface{}

Meta returns the metadata returned by the server during the last call to the collection's endpoint.

func (*Iterator) Next

func (it *Iterator) Next() bool

Next advances the iterator to the next object and returns true if there are more objects or false if the end of the collection has been reached.

type IteratorOption

type IteratorOption func(*Iterator) error

IteratorOption represents an option passed to an iterator.

func IteratorBatchSize

func IteratorBatchSize(n int) IteratorOption

IteratorBatchSize specifies the number of items that are retrieved in a single call to the backend.

func IteratorCursor

func IteratorCursor(cursor string) IteratorOption

IteratorCursor specifies a cursor for the iterator. The iterator will start at the point indicated by the cursor.

func IteratorDescriptorsOnly

func IteratorDescriptorsOnly(b bool) IteratorOption

IteratorDescriptorsOnly receives a boolean that indicate whether or not we want the backend to respond with object descriptors instead of the full objects.

func IteratorFilter

func IteratorFilter(filter string) IteratorOption

IteratorFilter specifies a filtering query that is sent to the backend. The backend will return items that comply with the condition imposed by the filter. The filter syntax varies depending on the collection being iterated.

func IteratorLimit

func IteratorLimit(n int) IteratorOption

IteratorLimit specifies a maximum number of items that will be returned by the iterator.

type Links struct {
	Self string `json:"self,omitempty"`
	Next string `json:"next,omitempty"`
}

Links contains links related to an API object.

type Metadata

type Metadata struct {
	// Dictionary where keys are the names of the Antivirus engines currently
	// supported by VirusTotal.
	Engines map[string]interface{} `json:"engines" yaml:"engines"`
	// Dictionary containing the relationships supported by each object type in
	// the VirusTotal API. Keys in this dictionary are object types, and values
	// are a list of RelationshipMeta structures with information about the
	// relationship.
	Relationships map[string][]RelationshipMeta `json:"relationships" yaml:"relationships"`
	Privileges    []string                      `json:"privileges" yaml:"privileges"`
}

Metadata describes the structure returned by /api/v3/metadata with metadata about VirusTotal, including the relationships supported by each object type.

type MonitorUploader

type MonitorUploader struct {
	// contains filtered or unexported fields
}

MonitorUploader represents a VT Monitor file upload.

func (*MonitorUploader) Replace

func (s *MonitorUploader) Replace(r io.Reader, monitorItemID string, progress chan<- float32) (*Object, error)

Replace modifies the contents of Monitor file identified by its monitorItemID. If the monitorItemID was previously deleted or does not exist a new file with the uploaded contents will be created. The file content is read from the r io.Reader and sent to Monitor. The function also sends a float32 through the progress channel indicating the percentage of the file that has been already uploaded. The progress channel can be nil if the caller is not interested in receiving upload progress updates. The received object is returned as soon as the file is uploaded.

func (*MonitorUploader) Upload

func (s *MonitorUploader) Upload(r io.Reader, monitorPath string, progress chan<- float32) (*Object, error)

Upload uploads a file to your VT Monitor account using a monitorPath destination path. The file content is read from the r io.Reader and sent to Monitor. If the remote path already exists the contents will be replaced. The function also sends a float32 through the progress channel indicating the percentage of the file that has been already uploaded. The progress channel can be nil if the caller is not interested in receiving upload progress updates. The received object is returned as soon as the file is uploaded.

type Object

type Object struct {
	// contains filtered or unexported fields
}

Object represents a VirusTotal API object.

func NewObject

func NewObject(objType string) *Object

NewObject creates a new object.

func NewObjectWithID

func NewObjectWithID(objType, id string) *Object

NewObjectWithID creates a new object with the specified ID.

func (*Object) Attributes

func (obj *Object) Attributes() []string

Attributes returns a list with the names of the object's attributes.

func (*Object) ContextAttributes

func (obj *Object) ContextAttributes() []string

ContextAttributes returns a list with the names of the object's context attributes. Context attributes are additional attributes that only make sense in a specific context. For example, when retrieving objects that are part of a relationship, the objects may have attributes that only make sense in the context of that relationship.

func (*Object) Get

func (obj *Object) Get(attr string) (interface{}, error)

Get attribute by name. It might include dots to fetch nested attributes. Example: 'vhash' Example for nested objects: 'pe_info.imphash' Example for arrays: 'tags.[0]' You can find additional attr modifiers in gojsonq github repository https://github.com/thedevsaddam/gojsonq/wiki/Queries#findpath The actual type for the returned value depends on attribute's type. Numeric attributes will be of type json.Number, use GetInt64 or GetFloat64 if you want one the result as an integer or float number.

func (*Object) GetBool

func (obj *Object) GetBool(attr string) (b bool, err error)

GetBool returns an attribute as a boolean. It returns the attribute's value or an error if the attribute doesn't exist or is not a boolean.

func (*Object) GetContext

func (obj *Object) GetContext(attr string) (interface{}, error)

GetContext gets a context attribute by name.

func (*Object) GetContextBool

func (obj *Object) GetContextBool(attr string) (b bool, err error)

GetContextBool returns a context attribute as a bool. It returns the attribute's value or an error if the attribute doesn't exist or is not a bool.

func (*Object) GetContextFloat64

func (obj *Object) GetContextFloat64(attr string) (float64, error)

GetContextFloat64 returns a context attribute as an float64. It returns the attribute's value or an error if the attribute doesn't exist or is not a number.

func (*Object) GetContextInt64

func (obj *Object) GetContextInt64(attr string) (int64, error)

GetContextInt64 returns a context attribute as an int64. It returns the attribute's value or an error if the attribute doesn't exist or is not a number.

func (*Object) GetContextString

func (obj *Object) GetContextString(attr string) (s string, err error)

GetContextString returns a context attribute as a string. It returns the attribute's value or an error if the attribute doesn't exist or is not a string.

func (*Object) GetFloat64

func (obj *Object) GetFloat64(attr string) (float64, error)

GetFloat64 returns an attribute as a float64. It returns the attribute's value or an error if the attribute doesn't exist or is not a number.

func (*Object) GetInt64

func (obj *Object) GetInt64(attr string) (int64, error)

GetInt64 returns an attribute as an int64. It returns the attribute's value or an error if the attribute doesn't exist or is not a number.

func (*Object) GetRelationship

func (obj *Object) GetRelationship(name string) (*Relationship, error)

GetRelationship returns a relationship by name. Only those relationships that you explicitly asked for in a call to GetObject can be obtained. You can ask by a relationship by including the "relationships" parameter in the URL used with GetObject.

Example:

f, _ := client.GetObject(vt.URL("files/%s?relationships=contacted_ips"))
// OK as "contacted_ip" was requested.
r, _ := f.GetRelationship("contacted_ips")
// Not OK, "contacted_urls" won't be present
r, _ := f.GetRelationship("contacted_urls")

func (*Object) GetString

func (obj *Object) GetString(attr string) (s string, err error)

GetString returns an attribute as a string. It returns the attribute's value or an error if the attribute doesn't exist or is not a string.

func (*Object) GetStringSlice

func (obj *Object) GetStringSlice(attr string) (s []string, err error)

GetStringSlice returns an attribute as a string slice. It returns the attribute's value or an error if the attribute doesn't exist or is not a string slice.

func (*Object) GetTime

func (obj *Object) GetTime(attr string) (t time.Time, err error)

GetTime returns an attribute as a time. It returns the attribute's value or an error if the attribute doesn't exist or is not a time.

func (*Object) ID

func (obj *Object) ID() string

ID returns the object's identifier.

func (*Object) MarshalJSON

func (obj *Object) MarshalJSON() ([]byte, error)

MarshalJSON marshals a VirusTotal API object.

func (*Object) MustGetBool

func (obj *Object) MustGetBool(attr string) bool

MustGetBool is like GetTime, but it panic in case of error.

func (*Object) MustGetFloat64

func (obj *Object) MustGetFloat64(attr string) float64

MustGetFloat64 is like GetFloat64, but it panic in case of error.

func (*Object) MustGetInt64

func (obj *Object) MustGetInt64(attr string) int64

MustGetInt64 is like GetInt64, but it panic in case of error.

func (*Object) MustGetString

func (obj *Object) MustGetString(attr string) string

MustGetString is like GetString, but it panic in case of error.

func (*Object) MustGetStringSlice

func (obj *Object) MustGetStringSlice(attr string) []string

MustGetStringSlice is like GetStringSlice, but it panic in case of error.

func (*Object) MustGetTime

func (obj *Object) MustGetTime(attr string) time.Time

MustGetTime is like GetTime, but it panic in case of error.

func (*Object) Relationships

func (obj *Object) Relationships() []string

Relationships returns a list with the names of the object's relationships.

func (*Object) Set

func (obj *Object) Set(attr string, value interface{}) error

Set the value for an attribute.

func (*Object) SetBool

func (obj *Object) SetBool(attr string, value bool) error

SetBool sets the value of a string attribute.

func (*Object) SetData

func (obj *Object) SetData(key string, val interface{})

SetData sets the value of a data field.

func (*Object) SetFloat64

func (obj *Object) SetFloat64(attr string, value float64) error

SetFloat64 sets the value of an integer attribute.

func (*Object) SetInt64

func (obj *Object) SetInt64(attr string, value int64) error

SetInt64 sets the value of an integer attribute.

func (*Object) SetString

func (obj *Object) SetString(attr, value string) error

SetString sets the value of a string attribute.

func (*Object) SetTime

func (obj *Object) SetTime(attr string, value time.Time) error

SetTime sets the value of a time attribute.

func (*Object) Type

func (obj *Object) Type() string

Type returns the object's type.

func (*Object) UnmarshalJSON

func (obj *Object) UnmarshalJSON(data []byte) error

UnmarshalJSON unmarshals a VirusTotal API object from data.

type Relationship

type Relationship struct {
	// contains filtered or unexported fields
}

Relationship contains information about a relationship between objects.

func (*Relationship) IsOneToOne

func (r *Relationship) IsOneToOne() bool

IsOneToOne returns true if this is a one-to-one relationship.

func (*Relationship) Objects

func (r *Relationship) Objects() []*Object

Objects return the objects in this relationship.

type RelationshipMeta

type RelationshipMeta struct {
	// Name of the relationship.
	Name string `json:"name" yaml:"name"`
	// A verbose description for the relationship.
	Description string `json:"description" yaml:"description"`
}

RelationshipMeta is the structure returned by each relationship from the /api/v3/metadata endpoint.

type Request

type Request struct {
	Data interface{} `json:"data"`
}

Request is the top level structure of an API request.

type RequestOption

type RequestOption func(*requestOptions)

RequestOption represents an option passed to some functions in this package.

func WithHeader

func WithHeader(header, value string) RequestOption

WithHeader specifies a header to be included in the request, it will override any header defined at client level.

type Response

type Response struct {
	Data  json.RawMessage        `json:"data"`
	Meta  map[string]interface{} `json:"meta"`
	Links Links                  `json:"links"`
	Error Error                  `json:"error"`
}

Response is the top level structure of an API response.

type URLScanner

type URLScanner struct {
	// contains filtered or unexported fields
}

URLScanner represents a URL scanner.

func (*URLScanner) Scan

func (s *URLScanner) Scan(url string) (*Object, error)

Scan sends a URL to VirusTotal for scanning. An analysis object is returned as soon as the URL is submitted.

type VTClient

type VTClient interface {
	Get(url *url.URL, options ...RequestOption) (*Response, error)
	Post(url *url.URL, req *Request, options ...RequestOption) (*Response, error)
	Patch(url *url.URL, req *Request, options ...RequestOption) (*Response, error)
	Delete(url *url.URL, options ...RequestOption) (*Response, error)
	GetData(url *url.URL, target interface{}, options ...RequestOption) (*Response, error)
	PostData(url *url.URL, data interface{}, options ...RequestOption) (*Response, error)
	DeleteData(url *url.URL, data interface{}, options ...RequestOption) (*Response, error)
	PostObject(url *url.URL, obj *Object, options ...RequestOption) error
	GetObject(url *url.URL, options ...RequestOption) (*Object, error)
	PatchObject(url *url.URL, obj *Object, options ...RequestOption) error
	DownloadFile(hash string, w io.Writer) (int64, error)
	Iterator(url *url.URL, options ...IteratorOption) (*Iterator, error)
	Search(query string, options ...IteratorOption) (*Iterator, error)
	GetMetadata() (*Metadata, error)
	NewFileScanner() *FileScanner
	NewURLScanner() *URLScanner
	NewMonitorUploader() *MonitorUploader
}

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL