2014-11-05 09:52:40 +00:00
|
|
|
#!/usr/bin/env python
|
2014-12-18 18:50:47 +00:00
|
|
|
|
2016-02-11 19:48:58 +00:00
|
|
|
# Copyright (c) 2014-present, Facebook, Inc.
|
2014-12-18 18:50:47 +00:00
|
|
|
# All rights reserved.
|
|
|
|
#
|
2019-02-19 18:52:19 +00:00
|
|
|
# This source code is licensed in accordance with the terms specified in
|
|
|
|
# the LICENSE file found in the root directory of this source tree.
|
2014-11-05 09:52:40 +00:00
|
|
|
|
|
|
|
from __future__ import absolute_import
|
|
|
|
from __future__ import division
|
2014-11-07 01:12:40 +00:00
|
|
|
from __future__ import print_function
|
2014-11-05 09:52:40 +00:00
|
|
|
from __future__ import unicode_literals
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import ast
|
2014-11-07 01:12:40 +00:00
|
|
|
import json
|
2014-11-05 09:52:40 +00:00
|
|
|
import logging
|
|
|
|
import os
|
|
|
|
import sys
|
2014-11-07 01:12:40 +00:00
|
|
|
import uuid
|
2015-07-15 20:18:41 +00:00
|
|
|
import subprocess
|
2014-11-05 09:52:40 +00:00
|
|
|
|
2015-06-01 22:53:52 +00:00
|
|
|
from gentable import *
|
2015-10-17 01:05:37 +00:00
|
|
|
from utils import platform
|
2015-06-01 22:53:52 +00:00
|
|
|
|
|
|
|
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
|
|
|
sys.path.append(SCRIPT_DIR + "/../tests")
|
2014-11-05 09:52:40 +00:00
|
|
|
|
|
|
|
# the log format for the logging module
|
|
|
|
LOG_FORMAT = "%(levelname)s [Line %(lineno)d]: %(message)s"
|
|
|
|
|
|
|
|
CANONICAL_PLATFORMS = {
|
2015-05-04 21:15:19 +00:00
|
|
|
"specs": "All Platforms",
|
2014-11-07 01:12:40 +00:00
|
|
|
"darwin": "Darwin (Apple OS X)",
|
|
|
|
"linux": "Ubuntu, CentOS",
|
2017-05-12 23:38:16 +00:00
|
|
|
"freebsd": "FreeBSD",
|
2016-06-23 23:04:11 +00:00
|
|
|
"posix": "POSIX-compatible Plaforms",
|
2017-05-12 23:38:16 +00:00
|
|
|
"windows": "Microsoft Windows",
|
|
|
|
"utility": "Utility",
|
2017-04-28 20:45:41 +00:00
|
|
|
"yara": "YARA",
|
2018-07-13 13:51:55 +00:00
|
|
|
"smart": "SMART",
|
2017-04-28 20:45:41 +00:00
|
|
|
"lldpd": "LLDPD",
|
2017-08-07 16:08:53 +00:00
|
|
|
"sleuthkit": "The Sleuth Kit",
|
2017-11-27 19:26:08 +00:00
|
|
|
"macwin": "MacOS and Windows",
|
|
|
|
"linwin": "Linux and Windows"
|
2014-11-05 09:52:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
TEMPLATE_API_DEFINITION = """
|
2015-02-05 04:02:32 +00:00
|
|
|
{
|
|
|
|
"tables": %s,
|
|
|
|
"events": [
|
|
|
|
]
|
|
|
|
}
|
2014-11-05 09:52:40 +00:00
|
|
|
"""
|
|
|
|
|
2014-11-14 23:04:19 +00:00
|
|
|
|
2014-11-07 01:12:40 +00:00
|
|
|
class NoIndent(object):
|
2014-11-14 23:04:19 +00:00
|
|
|
|
2014-11-07 01:12:40 +00:00
|
|
|
"""Special instance checked object for removing json newlines."""
|
2014-11-14 23:04:19 +00:00
|
|
|
|
2014-11-07 01:12:40 +00:00
|
|
|
def __init__(self, value):
|
|
|
|
self.value = value
|
2014-11-14 23:04:19 +00:00
|
|
|
if 'type' in self.value and isinstance(self.value['type'], DataType):
|
2014-11-14 06:16:28 +00:00
|
|
|
self.value['type'] = str(self.value['type'])
|
2014-11-07 01:12:40 +00:00
|
|
|
|
2014-11-14 23:04:19 +00:00
|
|
|
|
2014-11-07 01:12:40 +00:00
|
|
|
class Encoder(json.JSONEncoder):
|
2014-11-14 23:04:19 +00:00
|
|
|
|
2014-11-07 01:12:40 +00:00
|
|
|
"""
|
|
|
|
Newlines are such a pain in json-generated output.
|
|
|
|
Use this custom encoder to produce pretty json multiplexed with a more
|
|
|
|
raw json output within.
|
|
|
|
"""
|
2014-11-14 23:04:19 +00:00
|
|
|
|
2014-11-07 01:12:40 +00:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super(Encoder, self).__init__(*args, **kwargs)
|
|
|
|
self.kwargs = dict(kwargs)
|
|
|
|
del self.kwargs['indent']
|
|
|
|
self._replacement_map = {}
|
|
|
|
|
|
|
|
def default(self, o):
|
|
|
|
if isinstance(o, NoIndent):
|
|
|
|
key = uuid.uuid4().hex
|
|
|
|
self._replacement_map[key] = json.dumps(o.value, **self.kwargs)
|
|
|
|
return "@@%s@@" % (key,)
|
|
|
|
else:
|
|
|
|
return super(Encoder, self).default(o)
|
|
|
|
|
|
|
|
def encode(self, o):
|
|
|
|
result = super(Encoder, self).encode(o)
|
|
|
|
for k, v in self._replacement_map.iteritems():
|
|
|
|
result = result.replace('"@@%s@@"' % (k,), v)
|
|
|
|
return result
|
2014-11-05 09:52:40 +00:00
|
|
|
|
2014-11-14 23:04:19 +00:00
|
|
|
|
2015-06-01 22:53:52 +00:00
|
|
|
def gen_api_json(api):
|
2014-11-07 01:12:40 +00:00
|
|
|
"""Apply the api literal object to the template."""
|
2014-11-14 23:04:19 +00:00
|
|
|
api = json.dumps(
|
|
|
|
api, cls=Encoder, sort_keys=True, indent=1, separators=(',', ': ')
|
|
|
|
)
|
2014-11-07 01:12:40 +00:00
|
|
|
return TEMPLATE_API_DEFINITION % (api)
|
2014-11-05 09:52:40 +00:00
|
|
|
|
2014-11-14 23:04:19 +00:00
|
|
|
|
2014-11-05 09:52:40 +00:00
|
|
|
def gen_spec(tree):
|
2014-11-07 01:12:40 +00:00
|
|
|
"""Given a table tree, produce a literal of the table representation."""
|
|
|
|
exec(compile(tree, "<string>", "exec"))
|
2014-11-11 16:35:25 +00:00
|
|
|
columns = [NoIndent({
|
2014-11-14 23:04:19 +00:00
|
|
|
"name": column.name,
|
|
|
|
"type": column.type,
|
|
|
|
"description": column.description,
|
2015-06-05 18:20:24 +00:00
|
|
|
"options": column.options,
|
2014-11-14 23:04:19 +00:00
|
|
|
}) for column in table.columns()]
|
2014-11-07 01:12:40 +00:00
|
|
|
foreign_keys = [NoIndent({"column": key.column, "table": key.table})
|
2014-11-14 23:04:19 +00:00
|
|
|
for key in table.foreign_keys()]
|
2014-11-07 01:12:40 +00:00
|
|
|
return {
|
|
|
|
"name": table.table_name,
|
|
|
|
"columns": columns,
|
|
|
|
"foreign_keys": foreign_keys,
|
|
|
|
"function": table.function,
|
|
|
|
"description": table.description,
|
2015-02-09 01:40:35 +00:00
|
|
|
"attributes": table.attributes,
|
2015-06-01 22:53:52 +00:00
|
|
|
"examples": table.examples,
|
2014-11-07 01:12:40 +00:00
|
|
|
}
|
2014-11-05 09:52:40 +00:00
|
|
|
|
2014-11-14 23:04:19 +00:00
|
|
|
|
2015-01-21 19:50:42 +00:00
|
|
|
def gen_diff(api_old_path, api_new_path):
|
|
|
|
"""Quick and dirty way to view table API changes."""
|
|
|
|
with open(api_old_path, 'r') as fh:
|
|
|
|
api_old = json.loads(fh.read())
|
|
|
|
with open(api_new_path, 'r') as fh:
|
|
|
|
api_new = json.loads(fh.read())
|
|
|
|
|
|
|
|
# Prune table lists into maps
|
|
|
|
old_tables = {}
|
|
|
|
new_tables = {}
|
|
|
|
for category in api_new["tables"]:
|
|
|
|
for table in category["tables"]:
|
|
|
|
new_tables["%s:%s" % (category["name"], table["name"])] = table
|
|
|
|
for category in api_old["tables"]:
|
|
|
|
for table in category["tables"]:
|
|
|
|
old_tables["%s:%s" % (category["name"], table["name"])] = table
|
|
|
|
|
|
|
|
# Iterate backwards then forward to detect added/removed.
|
|
|
|
tables_added = []
|
|
|
|
tables_removed = []
|
|
|
|
columns_added = []
|
|
|
|
columns_removed = []
|
|
|
|
for name, table in new_tables.iteritems():
|
|
|
|
if name not in old_tables:
|
|
|
|
tables_added.append(name)
|
|
|
|
continue
|
|
|
|
for column in table["columns"]:
|
|
|
|
old_columns = [c["name"] for c in old_tables[name]["columns"]]
|
|
|
|
if column["name"] not in old_columns:
|
|
|
|
columns_added.append("%s:%s:%s:%s" % (category["name"],
|
2015-10-17 01:05:37 +00:00
|
|
|
table["name"], column["name"], column["type"]))
|
2015-01-21 19:50:42 +00:00
|
|
|
|
|
|
|
for name, table in old_tables.iteritems():
|
|
|
|
if name not in new_tables:
|
|
|
|
tables_removed.append(name)
|
|
|
|
continue
|
|
|
|
for column in table["columns"]:
|
|
|
|
new_columns = [c["name"] for c in new_tables[name]["columns"]]
|
|
|
|
if column["name"] not in new_columns:
|
|
|
|
columns_removed.append("%s:%s:%s:%s" % (category["name"],
|
2015-10-17 01:05:37 +00:00
|
|
|
table["name"], column["name"], column["type"]))
|
2015-01-21 19:50:42 +00:00
|
|
|
|
|
|
|
# Sort then pretty print (md) the changes.
|
|
|
|
tables_added.sort()
|
|
|
|
for name in tables_added:
|
2015-10-17 01:05:37 +00:00
|
|
|
print("Added table `%s` to %s" % tuple(name.split(":")[::-1]))
|
2015-01-21 19:50:42 +00:00
|
|
|
columns_added.sort()
|
|
|
|
for name in columns_added:
|
|
|
|
column = name.split(":")
|
2015-10-17 01:05:37 +00:00
|
|
|
print("Added column `%s` (`%s`) to table `%s`" % (column[2], column[3],
|
|
|
|
column[1]))
|
2015-01-21 19:50:42 +00:00
|
|
|
tables_removed.sort()
|
|
|
|
for name in tables_removed:
|
2015-10-17 01:05:37 +00:00
|
|
|
print("Removed table `%s` from %s" % tuple(name.split(":")[::-1]))
|
2015-01-21 19:50:42 +00:00
|
|
|
columns_removed.sort()
|
|
|
|
for name in columns_removed:
|
|
|
|
column = name.split(":")
|
2015-10-17 01:05:37 +00:00
|
|
|
print("Removed column `%s` (`%s`) from table `%s`" % (column[2],
|
|
|
|
column[3], column[1]))
|
2015-01-21 19:50:42 +00:00
|
|
|
|
|
|
|
|
2015-06-01 22:53:52 +00:00
|
|
|
def gen_api(tables_path, profile={}):
|
|
|
|
blacklist = None
|
|
|
|
blacklist_path = os.path.join(tables_path, "blacklist")
|
|
|
|
if os.path.exists(blacklist_path):
|
|
|
|
with open(blacklist_path, "r") as fh:
|
|
|
|
blacklist = fh.read()
|
|
|
|
|
|
|
|
categories = {}
|
|
|
|
for base, _, files in os.walk(tables_path):
|
|
|
|
for spec_file in files:
|
|
|
|
if spec_file[0] == '.' or spec_file.find("example") == 0:
|
|
|
|
continue
|
|
|
|
# Exclude blacklist specific file
|
|
|
|
if spec_file == 'blacklist':
|
|
|
|
continue
|
|
|
|
platform = os.path.basename(base)
|
2017-09-09 23:48:39 +00:00
|
|
|
# Exclude kernel tables
|
|
|
|
if platform in ['kernel']:
|
|
|
|
continue
|
2015-06-01 22:53:52 +00:00
|
|
|
platform_name = CANONICAL_PLATFORMS[platform]
|
|
|
|
name = spec_file.split(".table", 1)[0]
|
|
|
|
if platform not in categories.keys():
|
|
|
|
categories[platform] = {"name": platform_name, "tables": []}
|
|
|
|
with open(os.path.join(base, spec_file), "rU") as fh:
|
|
|
|
tree = ast.parse(fh.read())
|
|
|
|
table_spec = gen_spec(tree)
|
|
|
|
table_profile = profile.get("%s.%s" % (platform, name), {})
|
|
|
|
table_spec["profile"] = NoIndent(table_profile)
|
|
|
|
table_spec["blacklisted"] = is_blacklisted(table_spec["name"],
|
|
|
|
blacklist=blacklist)
|
|
|
|
categories[platform]["tables"].append(table_spec)
|
|
|
|
categories = [{"key": k, "name": v["name"], "tables": v["tables"]}
|
|
|
|
for k, v in categories.iteritems()]
|
|
|
|
return categories
|
|
|
|
|
2015-10-17 01:05:37 +00:00
|
|
|
|
2014-11-05 09:52:40 +00:00
|
|
|
def main(argc, argv):
|
2014-11-07 01:12:40 +00:00
|
|
|
parser = argparse.ArgumentParser("Generate API documentation.")
|
2014-11-14 23:04:19 +00:00
|
|
|
parser.add_argument(
|
2015-06-01 22:53:52 +00:00
|
|
|
"--debug", default=False, action="store_true",
|
|
|
|
help="Output debug messages (when developing)"
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
"--tables", default="specs",
|
2014-11-14 23:04:19 +00:00
|
|
|
help="Path to osquery table specs"
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
"--profile", default=None,
|
|
|
|
help="Add the results of a profile summary to the API."
|
|
|
|
)
|
2015-01-21 19:50:42 +00:00
|
|
|
parser.add_argument(
|
|
|
|
"--diff", default=False, action="store_true",
|
|
|
|
help="Compare API changes API_PREVIOUS API_CURRENT"
|
|
|
|
)
|
2015-07-15 20:18:41 +00:00
|
|
|
parser.add_argument(
|
|
|
|
"--output", default=False, action="store_true",
|
|
|
|
help="Create output file as the version tagged."
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
"--directory", default=".",
|
|
|
|
help="Directory to use for the output file."
|
|
|
|
)
|
2015-01-21 19:50:42 +00:00
|
|
|
parser.add_argument("vars", nargs="*")
|
2015-06-04 18:46:19 +00:00
|
|
|
args = parser.parse_args()
|
2014-11-07 01:12:40 +00:00
|
|
|
|
2015-06-01 22:53:52 +00:00
|
|
|
if args.debug:
|
|
|
|
logging.basicConfig(format=LOG_FORMAT, level=logging.DEBUG)
|
|
|
|
else:
|
|
|
|
logging.basicConfig(format=LOG_FORMAT, level=logging.INFO)
|
2014-11-07 01:12:40 +00:00
|
|
|
|
2015-01-21 19:50:42 +00:00
|
|
|
if args.diff:
|
|
|
|
if len(args.vars) < 2:
|
|
|
|
logging.error("If using --diff you must supply API_OLD API_NEW")
|
|
|
|
exit(1)
|
|
|
|
gen_diff(args.vars[0], args.vars[1])
|
|
|
|
exit(0)
|
|
|
|
|
2014-11-07 01:12:40 +00:00
|
|
|
if not os.path.exists(args.tables):
|
|
|
|
logging.error("Cannot find path: %s" % (args.tables))
|
|
|
|
exit(1)
|
|
|
|
|
|
|
|
profile = {}
|
|
|
|
if args.profile is not None:
|
|
|
|
if not os.path.exists(args.profile):
|
|
|
|
logging.error("Cannot find path: %s" % (args.profile))
|
|
|
|
exit(1)
|
|
|
|
with open(args.profile, "r") as fh:
|
|
|
|
try:
|
|
|
|
profile = json.loads(fh.read())
|
|
|
|
except Exception as e:
|
|
|
|
logging.error("Cannot parse profile data: %s" % (str(e)))
|
|
|
|
exit(2)
|
|
|
|
|
2015-10-17 01:05:37 +00:00
|
|
|
# Read in the optional list of blacklisted tables, then generate
|
|
|
|
# categories.
|
2015-06-01 22:53:52 +00:00
|
|
|
api = gen_api(args.tables, profile)
|
2015-07-15 20:18:41 +00:00
|
|
|
|
|
|
|
# Output file will be the version with json extension, otherwise stdout
|
|
|
|
if args.output:
|
2015-10-17 01:05:37 +00:00
|
|
|
print('[+] creating tables json')
|
|
|
|
cmd = ['git', 'describe', '--tags', 'HEAD']
|
|
|
|
proc = subprocess.Popen(
|
|
|
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
|
|
)
|
|
|
|
out, err = proc.communicate()
|
|
|
|
output_file = out.split("\n")[0] + ".json"
|
|
|
|
if args.directory[-1:] == '/':
|
|
|
|
output_path = args.directory + output_file
|
|
|
|
else:
|
|
|
|
output_path = args.directory + '/' + output_file
|
|
|
|
|
|
|
|
with open(output_path, 'w') as f:
|
|
|
|
print(gen_api_json(api), file=f)
|
|
|
|
print('[+] tables json file created at %s' % (output_path))
|
2015-07-15 20:18:41 +00:00
|
|
|
else:
|
2015-10-17 01:05:37 +00:00
|
|
|
print(gen_api_json(api))
|
2014-11-05 09:52:40 +00:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2015-06-01 22:53:52 +00:00
|
|
|
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
2014-11-05 09:52:40 +00:00
|
|
|
main(len(sys.argv), sys.argv)
|