Initial import from internal repository.

This commit is contained in:
Michael Scovetta 2016-10-17 12:37:59 -07:00
Коммит 096c9aea57
17 изменённых файлов: 1347 добавлений и 0 удалений

1
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1 @@
rules/*

51
CONTRIBUTING.md Normal file
Просмотреть файл

@ -0,0 +1,51 @@
## Contributing Issues
### Before Submitting an Issue
First, please do a search of [open issues](https://github.com/Microsoft/DevSkim-SublimeText/issues) to see
if the issue or feature request has already been filed. Use this
[query](https://github.com/Microsoft/DevSkim-SublimeText/issues?q=is%3Aopen+is%3Aissue+label%3Afeature-request+sort%3Areactions-%2B1-desc)
to search for the most popular feature requests.
If you find your issue already exists, make relevant comments and add your
[reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments). Use a reaction in place of a "+1" comment.
👍 - upvote
👎 - downvote
The DevSkim project is distributed across multiple repositories, so try to file the issue against the correct repository:
* [DevSkim-SublimeText](https://github.com/Microsoft/DevSkim-SublimeText/) - Sublime Text Plugin
* [DevSkim-VSCode](https://github.com/Microsoft/DevSkim-VSCode/) - VSCode Plugin
* [DevSkim-VisualStudio](https://github.com/Microsoft/DevSkim-VisualStudio/) - Visual Studio Plugin
* [DevSkim-Rules](https://github.com/Microsoft/DevSkim-Rules/) - Common Rules
If your issue is a question then please ask the question on [Stack Overflow](https://stackoverflow.com/questions/tagged/devskim)
using the tag `devskim`.
If you cannot find an existing issue that describes your bug or feature, submit an issue using the guidelines below.
## Writing Good Bug Reports and Feature Requests
File a single issue per problem and feature request.
* Do not enumerate multiple bugs or feature requests in the same issue.
* Do not add your issue as a comment to an existing issue unless it's for the identical input. Many issues look similar, but have different causes.
* Turning on `debug` mode can be helpful in providing more information (Package Settings | DevSkim | Settings - User | set `"debug": true`)
The more information you can provide, the more likely someone will be successful reproducing the issue and finding a fix. Therefore:
* Provide reproducible steps, what the result of the steps was, and what you would have expected.
* A description of what you expect to happen
* Code that demonstrates the issue, when providing a code snippet also include it in source and not only as an image
* Version of DevSkim and Sublime Text
* Errors in the Sublime Text Console (View | Show Console)
Don't feel bad if we can't reproduce the issue and ask for more information!
## Contributing Fixes
If you are interested in fixing issues and contributing directly to the code base,
please see the document [How to Contribute](https://github.com/Microsoft/DevSkim-SublimeText/wiki/How-to-Contribute).

Просмотреть файл

@ -0,0 +1,8 @@
[
{
"keys": [ "ctrl+shift+d" ],
"command": "dev_skim_analyze",
"context": [
]
}
]

Просмотреть файл

@ -0,0 +1,8 @@
[
{
"keys": [ "super+shift+d" ],
"command": "dev_skim_analyze",
"context": [
]
}
]

Просмотреть файл

@ -0,0 +1,8 @@
[
{
"keys": [ "ctrl+shift+d" ],
"command": "dev_skim_analyze",
"context": [
]
}
]

945
DevSkim.py Normal file
Просмотреть файл

@ -0,0 +1,945 @@
"""
Copyright (c) 2016 Microsoft. All rights reserved.
Licensed under the MIT License. See LICENSE.txt in the project root for license information.
DevSkim Sublime Text Plugin
"""
import datetime
import json
import logging
import re
import time
import os
import traceback
import webbrowser
try:
import sublime
import sublime_plugin
except:
print("Unable to import Sublime Text modules. DevSkim must be run within Sublime Text.")
import sys
sys.exit(1)
# Non-configurable settings
MIN_ST_VERSION = 3114
MARK_FLAGS = sublime.DRAW_NO_FILL | sublime.HIDE_ON_MINIMAP
LOG_FORMAT = '%(asctime)-15s: %(levelname)s: %(name)s.%(funcName)s: %(message)s'
SEVERITY_LIST = ["critical", "important", "moderate", "low",
"defense-in-depth", "informational"]
PACKAGE_DIR = os.path.join(sublime.packages_path(), os.path.basename(os.path.dirname(__file__)))
RULE_DIRECTORY = ['default', 'custom']
# Minimum Sublime Text version we support
if int(sublime.version()) < MIN_ST_VERSION:
raise RuntimeError('DevSkim requires Sublime Text 3 v%d or later.' %
MIN_ST_VERSION)
# Global Properties
devskim_event_listener = None
# Global logger
logger = logging.getLogger(__name__)
# Cache of user settings (sublime-provided)
user_settings = None
# Global objects, used for caching
rules = []
# Was the applies_to_ext_mapping already loaded from syntax files?
applies_to_ext_mapping_initialized = False
# Mapping between symbolic names ("csharp") and what they actually mean.
# This map is updated to include runtime syntax files, so this is just
# a starting point.
applies_to_ext_mapping = {
"csharp": {
"syntax": ["Packages/C#/C#.sublime-syntax"],
"extensions": ["cs"]
},
"aspnet": {
"syntax": [],
"extensions": ["aspx"]
},
"python": {
"syntax": ["Packages/Python/Python.sublime-syntax"],
"extensions": ["py"]
},
"c": {
"syntax": ["Packages/C++/C.sublime-syntax"],
"extensions": ["c", "h"]
},
"cpp": {
"syntax": ["Packages/C++/C++.sublime-syntax"],
"extensions": ["c", "h", "cpp", "hpp"]
},
"javascript": {
"syntax": ["Packages/JavaScript/JavaScript.sublime-syntax"],
"extensions": ["js"]
},
"ruby": {
"syntax": ["Packages/Ruby/Ruby.sublime-syntax"],
"extensions": ["rb", "erb"]
},
"java": {
"syntax": ["Packages/Java/Java.sublime-syntax"],
"extensions": ["java"]
},
"php": {
"syntax": ["Packages/PHP/PHP.sublime-syntax"],
"extensions": ["php"]
},
"objective-c": {
"syntax": ["Packages/Objective-C/Objective-C.sublime-syntax"],
"extensions": ["m", "mm", "h", "c"]
},
"ios": {
"syntax": ["Packages/Objective-C/Objective-C.sublime-syntax"],
"extensions": ["m", "mm", "h", "c"]
},
"swift": {
"syntax": ["Packages/Swift/Swift.sublime-syntax"],
"extensions": ["swift"]
},
"java": {
"syntax": ["Packages/Java/Java.sublime-syntax"],
"extensions": ["java"]
},
"powershell": {
"syntax": ["Packages/PowerShell/PowerShell.sublime-syntax"],
"extensions": ["ps1"]
},
"swift": {
"syntax": ["Packages/Swift/Swift.sublime-syntax"],
"extensions": ["swift"]
}
}
# Currently marked regions
marked_regions = []
# Cached stylesheet content
stylesheet_content = ""
# Cache suppress days
suppress_days = []
class DevSkimEventListener(sublime_plugin.EventListener):
"""Handles events from Sublime Text."""
# Reference to the current view
view = None
def __init__(self, *args, **kwargs):
"""Initialize events listener."""
super(DevSkimEventListener, self).__init__(*args, **kwargs)
logger.debug("DevSkimEventListener.__init__()")
def lazy_initialize(self):
"""Perform lazy initialization."""
global rules, user_settings, stylesheet_content, suppress_days
global applies_to_ext_mapping, applies_to_ext_mapping_initialized
# logger.debug('lazy_initialize')
if not user_settings:
user_settings = sublime.load_settings('DevSkim.sublime-settings')
if not user_settings:
logger.warning("Unable to load DevSkim settings.")
return
user_settings.clear_on_change('DevSkim')
user_settings.add_on_change('DevSkim', self.lazy_initialize)
if not rules or len(rules) == 0:
self.load_rules()
if not applies_to_ext_mapping_initialized:
self.load_syntax_mapping()
applies_to_ext_mapping_initialized = True
if not stylesheet_content:
if not user_settings:
logger.warning("Unable to load DevSkim settings.")
return
css_file = user_settings.get('style', 'css/dark.css')
stylesheet_content = sublime.load_resource('Packages/DevSkim/%s' % css_file)
stylesheet_content = stylesheet_content.replace('\r\n', '\n')
logger.debug("Stylesheet: [%s]", stylesheet_content)
if not suppress_days:
if not user_settings:
logger.warning("Unable to load DevSkim settings.")
return
suppress_days = user_settings.get('suppress_days', [90, 365, -1])
logger.debug('Suppress Days: %s', suppress_days)
# Initialize the logger
if len(logger.handlers) == 0:
root_logger = logging.getLogger()
console = logging.StreamHandler()
console.setFormatter(logging.Formatter(LOG_FORMAT))
if user_settings.get('debug', False):
print("set to debug")
root_logger.setLevel(logging.DEBUG)
else:
root_logger.setLevel(logging.WARNING)
logger.handlers = []
logger.addHandler(console)
def clear_regions(self, view):
"""Clear all regions."""
global marked_regions, finding_list
logger.debug("clear_regions()")
if view:
view.erase_regions("devskim-marks")
marked_regions = []
finding_list = []
def on_selection_modified(self, view):
"""Handle selection change events."""
self.lazy_initialize()
# logger.debug("on_selection_modified()")
global marked_regions, finding_list
try:
# Get the current region (cursor start)
cur_region = view.sel()[0]
for region in marked_regions:
if region.contains(cur_region):
for finding in finding_list:
if finding.get("match_region") != region:
continue
rule = finding.get('rule')
view.set_status("DevSkim", "DevSkim: %s" % rule.get("name"))
return
# We're not in a marked region, clear the status bar
view.erase_status("DevSkim")
except Exception as msg:
logger.warning("Error in on_selection_modified: %s", msg)
def on_navigate(self, command):
"""Handle navigation events."""
global finding_list, rules
self.lazy_initialize()
logger.debug("on_navigate(%s)", command)
if not command:
return
command = command.strip()
# Open a regular URL in the user's web browser
if re.match("^https?://", command, re.IGNORECASE):
webbrowser.open_new(command)
# Special commands, intercept and perform the fix
if command.startswith('#fixit'):
rule_id, fixid = command.split(',')[1:]
fixid = int(fixid)
for finding in finding_list:
rule = finding.get('rule')
if rule.get('id') == rule_id:
region_start = finding.get("match_region").begin()
contents = self.view.substr(self.view.line(region_start))
fixit = rule.get('fix_it')
if not fixit or fixid >= len(fixit):
continue
fixit = fixit[fixid]
if fixit['type'] == 'regex_substitute':
search = fixit['search']
replace = fixit['replace']
result = re.sub(search, replace, contents)
logger.debug("Result of search/replace was [%s]", result)
self.view.run_command('replace_text', {
'a': self.view.line(region_start).a,
'b': self.view.line(region_start).b,
'result': result
})
self.view.hide_popup()
# Only fix once
break
elif command.startswith('#add-suppression'):
rule_id, region_start, suppress_day = command.split(',')[1:]
cur_line = self.view.line(int(region_start))
# Ignore suppression for this many days from today
try:
suppress_day = int(suppress_day)
except Exception as msg:
suppress_day = -1
if suppress_day == -1:
comment = " DevSkim: ignore %s " % rule_id
else:
until_day = datetime.date.today() + datetime.timedelta(days=suppress_day)
comment = " DevSkim: ignore %s until %s " % (rule_id, until_day.strftime("%Y-%m-%d"))
# Add the pragma/comment at the end of the current line
self.view.run_command('insert_text', {
'a': cur_line.b,
'result': comment
})
# Now highlight that new section
prev_sel = self.view.sel()
self.view.sel().clear()
self.view.sel().add(sublime.Region(cur_line.b + 1, cur_line.b + len(comment)))
# Now make it a block comment
self.view.run_command('toggle_comment', {'block': True})
self.view.sel().clear()
self.view.sel().add_all(prev_sel)
self.view.hide_popup()
else:
logger.debug("Invalid command: %s", command)
def on_modified(self, view):
"""Handle immedate analysis (on keypress)."""
global user_settings
self.lazy_initialize()
if user_settings.get('show_highlights_on_modified', False):
try:
self.analyze_current_view(view, show_popup=False, single_line=True)
except Exception as msg:
logger.warning("Error analyzing current view: %s", msg)
def on_load_async(self, view):
"""Handle asynchronous loading event."""
global user_settings
self.lazy_initialize()
if user_settings.get('show_highlights_on_load', False):
try:
self.analyze_current_view(view, show_popup=False)
except Exception as msg:
logger.warning("Error analyzing current view: %s", msg)
def on_post_save_async(self, view):
"""Handle post-save events."""
global user_settings
self.lazy_initialize()
logger.debug("on_post_save_async()")
if user_settings.get('show_findings_on_save', True):
try:
self.analyze_current_view(view)
except Exception as msg:
logger.warning("Error analyzing current view: %s", msg)
def analyze_current_view(self, view, show_popup=True, single_line=False):
"""Kick off the analysis."""
global marked_regions, finding_list, user_settings
if view is None or view.window() is None:
# Early abort if we don't have a View and Window
return
window = view.window()
self.lazy_initialize()
logger.debug("analyze_current_view()")
# Time the execution of this function
start_time = time.clock()
self.view = view
# Check for files too large to scan
max_size = user_settings.get('max_size', 524288)
if 0 < max_size < view.size():
logger.debug("File was too large to scan (%d bytes)", view.size())
return
window = self.view.window()
if window is None:
return
extension = window.extract_variables().get('file_extension', '')
show_severity = user_settings.get('show_severity', SEVERITY_LIST)
# File syntax type
syntax = view.settings().get('syntax')
# Reset the UI (except if analyzing only a single line)
if not single_line:
self.clear_regions(view)
finding_list = []
# Grab the full file as a region
full_region = sublime.Region(0, view.size())
# Reset the marked regions for this view
if not single_line:
marked_regions = []
if single_line:
file_contents = view.substr(view.line(view.sel()[0]))
logger.debug("Single line: [%s]", file_contents)
offset = view.line(view.sel()[0]).begin()
else:
# Send the entire file over to DevSkim.execute
file_contents = view.substr(full_region)
logger.debug("File contents, size [%d]", len(file_contents))
offset = 0
result_list = []
try:
_v = window.extract_variables()
filename = _v.get('file', '').replace('\\', '/')
force_analyze = (extension == 'test' and
'/DevSkim/' in filename and
'/tests/' in filename)
result_list = self.execute(file_contents, extension, syntax, show_severity, force_analyze, offset)
logger.debug("DevSkim retured: [%s]", result_list)
except Exception as ex:
logger.warning("Error executing DevSkim: [%s]", ex)
traceback.print_exc()
# rule['overrides'] logic
overrides_list = []
for result in result_list:
if 'overrides' in result['rule']:
overrides_list += result['rule']['overrides']
original_result_list_count = len(result_list)
for rule_id in overrides_list:
# Remove all results that match a id in the overrides_list
result_list = list(filter(lambda r: r['rule']['id'] != rule_id,
result_list))
if original_result_list_count > len(result_list):
logger.debug("Reduced result list from [%d] to [%d] by overriding rules",
(original_result_list_count, len(result_list)))
for result in result_list:
# Narrow down to just the matching part of the line
scope_name = view.scope_name(result.get('match_region').begin())
scope_list = ["%s." % s for s in result.get('scope_list')]
logger.debug("Current Scope: [%s], Applies to: %s" % (scope_name, scope_list))
# Don't actually include if we're in a comment, or a quoted string, etc.
if any([x in scope_name for x in scope_list]) or len(scope_list) == 0:
marked_regions.append(result.get('match_region'))
finding_list.append(result)
logger.debug("Set marked regions to: %s" % marked_regions)
# Add a region (squiggly underline)
view.add_regions("devskim-marks",
marked_regions,
"string",
user_settings.get('gutter_mark', 'dot'),
flags=MARK_FLAGS)
shown_finding_list = []
# Sort the findings
sort_by = user_settings.get('sort_results_by', 'line_number')
if sort_by == 'severity':
finding_list.sort(key=lambda s: SEVERITY_LIST.index(s.get('rule').get('severity')))
elif sort_by == 'line_number':
finding_list.sort(key=lambda s: view.rowcol(s.get('match_region').begin())[0])
for finding in finding_list:
rule = finding.get('rule')
region_start = finding.get("match_region").begin()
region = view.rowcol(region_start)
severity = rule.get('severity', 'informational')
severity = self.severity_abbreviation(severity).upper()
shown_finding_list.append([rule.get("name"), "%d: [%s] %s" %
(region[0] + 1, severity,
view.substr(view.line(region_start)).strip())])
if show_popup:
window.show_quick_panel(shown_finding_list, self.on_selected_result)
end_time = time.clock()
logger.debug("Elapsed time: %f" % (end_time - start_time))
def on_selected_result(self, index):
"""Handle when the user clicks on a finding from the popup menu."""
global finding_list, stylesheet_content, user_settings
self.lazy_initialize()
if index == -1:
return
chosen_item = finding_list[index]
target_region = chosen_item.get('match_region')
self.view.sel().clear()
self.view.sel().add(target_region)
self.view.show(target_region)
rule = chosen_item['rule']
# Create a guidance popup
guidance = ['<body id="devskim-popup">']
if stylesheet_content:
guidance.append('<style>%s</style>' % stylesheet_content)
guidance.append("<h3>%s</h3>" % rule.get('name', 'Missing rule name'))
guidance.append('<p>%s</p>' % rule.get('description', '<i>Missing rule description</i>'))
if rule.get('replacement'):
guidance.append('<p>%s</p>' % rule['replacement'])
if rule.get('fix_it'):
guidance.append('<h3>Options:</h3>')
guidance.append("<ul>")
for fixid, fix in enumerate(rule.get('fix_it')):
guidance.append('<li>Auto-Fix: <a href="#fixit,%s,%d">%s</a></li>' %
(rule.get('id'), fixid, fix.get('name')))
guidance.append("</ul>")
# Supression links
this_suppression_links = []
all_suppression_links = []
for suppress_day in suppress_days:
suppress_day_str = "%d days" % suppress_day if suppress_day != -1 else "permanently"
this_suppression_links.append('[ <a href="#add-suppression,%s,%d,%d">%s</a> ] ' %
(rule.get('id'), target_region.a, suppress_day, suppress_day_str))
all_suppression_links.append('[ <a href="#add-suppression,all,%d,%d">%s</a> ] ' %
(target_region.a, suppress_day, suppress_day_str))
guidance.append("<ul>")
if user_settings.get('allow_suppress_specific_rules'):
guidance.append("<li>Supress this rule for: %s</li>" % (''.join(this_suppression_links)))
if user_settings.get('allow_suppress_all_rules'):
guidance.append("<li>Supress all rules for: %s</li>" % (''.join(all_suppression_links)))
guidance.append('</ul>')
if rule.get('rule_info'):
guidance.append('<h4><a href="%s">Learn More...</a></h4>' % rule.get('rule_info'))
elif user_settings.get('debug', False):
guidance.append('<h4>Rule: %s</h4>' % rule.get('id'))
guidance.append('</body>')
sublime.set_timeout_async(lambda: self.ds_show_popup(''.join(guidance),
location=target_region.end(),
max_width=860,
max_height=560,
on_navigate=self.on_navigate,
flags=sublime.HTML), 0)
def load_rules(self, force_reload=False):
"""Reload ruleset from the JSON configuration files."""
global rules, user_settings
if not force_reload and len(rules) > 0:
logger.debug("Rules already loaded, no need to reload.")
return False
logger.debug('DevSkimEngine.load_rules()')
if not user_settings:
logger.warning("Settings not found, cannot load rules.")
return False
json_filenames = sublime.find_resources("*.json")
rule_filenames = []
for filename in json_filenames:
for _dir in RULE_DIRECTORY:
if 'DevSkim/rules/%s' % _dir in filename:
rule_filenames.append(filename)
# Remove duplicates
rule_filenames = list(set(rule_filenames))
logger.debug("Loaded %d rule files" % len(rule_filenames))
# We load rules from each rule file here.
rules = []
for rule_filename in rule_filenames:
try:
rules += json.loads(sublime.load_resource(rule_filename))
except Exception as msg:
logger.warning("Error loading [%s]: %s" % (rule_filename, msg))
if user_settings.get('debug', False):
sublime.error_message("Error loading [%s]" % rule_filename)
# Now we load custom rules on top of this.
try:
for rule_filename in user_settings.get('custom_rules', []):
try:
env_variables = sublime.active_window().extract_variables()
rule_filename = sublime.expand_variables(rule_filename,
env_variables)
with open(rule_filename, encoding='utf-8') as crf:
custom_rule = json.loads(crf.read())
rules += custom_rule
except Exception as msg:
logger.warning("Error opening [%s]: %s" %
(rule_filename, msg))
except Exception as msg:
logger.warning("Error opening custom rules: %s" % msg)
if not user_settings:
logger.warning("Settings not found, cannot load rules.")
return False
for rule_id in user_settings.get('suppress_rules', []):
try:
rules = filter(lambda s: s.get('id', '') != rule_id, rules)
except Exception as msg:
logger.warning("Error suppressing rules for %s: %s" %
(rule_id, msg))
# Only include active rules
rules = list(filter(lambda x: x.get('active', True), rules))
# Filter by tags, if specified, convert all to lowercase
show_only_tags = set([k.lower().strip()
for k in user_settings.get('show_only_tags', [])])
if show_only_tags:
def filter_func(x):
return set([k.lower().strip()
for k in x.get('tags', [])]) & show_only_tags
rules = list(filter(filter_func, rules))
logger.debug("Loaded %d rules" % len(rules))
# for rule in rules:
# for pattern in rule.get('patterns'):
# print("%s\t%s\t%s" % (pattern.get('pattern'), rule.get('severity'), rule.get('name')))
return True
def load_syntax_mapping(self):
"""Load syntax content from various installed packages."""
global applies_to_ext_mapping
logger.debug('DevSkimEngine.load_syntax_mapping()')
for k, v in applies_to_ext_mapping.items():
applies_to_ext_mapping[k]['syntax'] = \
set(applies_to_ext_mapping[k]['syntax'])
applies_to_ext_mapping[k]['extensions'] = \
set(applies_to_ext_mapping[k]['extensions'])
# Iterate through all syntax files
for filename in sublime.find_resources("*.sublime-syntax"):
# Load the contents
syntax_file = sublime.load_resource(filename)
applies_to_name = None
for k, v in applies_to_ext_mapping.items():
for syntax in v.get('syntax', []):
if syntax == filename:
applies_to_name = k
break
if not applies_to_name:
continue # We need to have these defined first.
# Look for all extensions
in_file_extensions = False
for line in syntax_file.splitlines():
# Clean off wittepsace
line = line.strip()
# Are we entering?
if line == 'file_extensions:':
in_file_extensions = True
continue
# Are we in the file extension section?
if in_file_extensions:
if line.startswith('- '):
# Add the extension to the mapping
extension = line.replace("- ", "").strip()
applies_to_ext_mapping[applies_to_name]['extensions'].add(extension)
else:
in_file_extensions = False
break
def execute(self, file_contents, extension=None, syntax=None,
severities=None, force_analyze=False, offset=0):
"""Execute all of the rules against a given string of text."""
global rules, applies_to_ext_mapping
logger.debug("execute([len=%d], [%s], [%s], [%s], [%d]" %
(len(file_contents), extension, syntax, force_analyze, offset))
if not file_contents:
return []
syntax_types = set([]) # Example: ["csharp"], from the file itself
# TODO Cache this elsewhere, silly to do every time, I think.
for k, v in applies_to_ext_mapping.items():
if (v.get('syntax', None) == syntax or
extension in v.get('extensions', [])):
syntax_types.add(k)
result_list = []
for rule in rules:
# Don't even scan for rules that we don't care about
if not force_analyze and rule.get('severity', 'critical') not in severities:
logger.debug("Ignoring rule [%s] due to severity." % rule.get('id', ''))
continue
# No syntax means "match any syntax"
rule_applies_to = rule.get('applies_to', [])
if (force_analyze or
not rule_applies_to or
set(rule_applies_to) & set(syntax_types) or
'.%s' % extension in rule_applies_to):
for pattern_dict in rule['patterns']:
# Secondary applicability
pattern_applies_to = set(pattern_dict.get('applies_to', []))
if (pattern_applies_to and
not force_analyze and
not pattern_applies_to & set(syntax_types) and
not '.%s' % extension in pattern_applies_to):
logger.debug("Ignoring rule [%s], applicability check." % rule.get('id', ''))
continue
pattern_str = pattern_dict.get('pattern')
start = end = -1
orig_pattern_str = pattern_str
if pattern_dict.get('type') == 'substring':
pattern_str = re.escape(pattern_str)
elif pattern_dict.get('type') == 'string':
pattern_str = r'\b%s\b' % re.escape(pattern_str)
elif pattern_dict.get('type') == 'regex':
pass
elif pattern_dict.get('type') == 'regex_word':
pattern_str = r'\b%s\b' % pattern_str
else:
logger.warning("Invalid pattern type [%s] found." %
pattern_dict.get('type'))
continue
scope_list = pattern_dict.get('subtype', [])
modifiers = pattern_dict.get('modifiers', [])
flags = 0
if modifiers:
modifiers = map(lambda s: s.lower(), modifiers)
# The rule here is that if a modifier is passed,
# then that's the modifier used. Otherwise, we do
# IGNORECASE | MULTILINE.
if 'dotall' in modifiers:
flags |= re.DOTALL
if 'multiline' in modifiers:
flags |= re.MULTILINE
if 'ignorecase' in modifiers:
flags |= re.IGNORECASE
else:
# Default
flags = re.IGNORECASE | re.MULTILINE
for match in re.finditer(pattern_str, file_contents, flags):
if not match:
logger.debug("re.finditer([%s], [%s]) => [-]" %
(pattern_str, len(file_contents)))
continue # Does this ever happen?
logger.debug("re.finditer([%s], [%s]) => [%d, %d]" %
(pattern_str, len(file_contents),
match.start(), match.end()))
start = match.start()
end = match.end()
# Check for per-row suppressions
row_number = self.view.rowcol(start)
line_list = [
self.view.substr(self.view.line(start))
]
if row_number[0] > 0: # Special case, ignore
prev_line = self.view.text_point(row_number[0] - 1, 0)
line_list.append(self.view.substr(self.view.line(prev_line)))
if self.is_suppressed(rule, line_list):
continue # Don't add the result to the list
result_list.append({
'rule': rule,
'match_content': match.group(),
'match_region': sublime.Region(start + offset, end + offset),
'match_start': start + offset,
'match_end': end + offset,
'pattern': orig_pattern_str,
'scope_list': scope_list
})
return result_list
def severity_abbreviation(self, severity):
"""Convert a severity name into an abbreviation."""
if severity is None:
return ""
severity = severity.strip().lower()
if severity == "critical":
return "crit"
elif severity == "important":
return "imp."
elif severity == "moderate":
return "mod."
elif severity == "low":
return "low"
elif severity == "defense-in-depth":
return "did."
elif severity == "informational":
return "info"
return ""
def is_suppressed(self, rule, lines):
"""Should the result be suppressed based on the given rule and line content."""
global user_settings
# self.lazy_initialize()
if not rule or not lines:
return False
# Are suppression rules enabled at all?
if not user_settings.get('allow_suppress_specific_rules') and not user_settings.get('allow_suppress_all_rules'):
logger.debug('Suppression disabled via config, nothing to do.')
return False
for line in lines:
line = line.lower()
if 'devskim:' not in line:
continue
if user_settings.get('allow_suppress_specific_rules'):
for match in re.finditer(r'ignore ([^\s]+)\s+until (\d{4}-\d{2}-\d{2})', line):
if match.group(1) in ['all', rule.get('id', 'all').lower()]:
suppress_until = match.group(2)
try:
suppress_until = datetime.datetime.strptime(suppress_until, '%Y-%m-%d')
if datetime.date.today() < suppress_until.date():
logger.debug('Ignoring rule [%s] due to limited suppression.', rule.get('id'))
return True
except Exception as msg:
logger.debug("Error parsing suppression date: %s", msg)
if not user_settings.get('allow_suppress_all_rules'):
for match in re.finditer(r'ignore ([^\s]+)\s*(?!\s+until \d{4}-\d{2}-\d{2})', line):
if match.group(1) in ['all', rule.get('id', 'all').lower()]:
logger.debug('Ignoring rule [%s] due to unlimited suppression.', rule.get('id'))
return True
return False
def ds_show_popup(self, content, flags, location, max_width, max_height,
on_navigate=None, on_hide=None, repeat_duration_ms=50):
"""Delay-load a popup to give the UI time to get to the scrolled position."""
if self.view.is_popup_visible():
return # OK, a popup is already being shown
# Try to show the popup
self.view.show_popup(content=content,
flags=flags,
location=location,
max_width=max_width,
max_height=max_height,
on_navigate=on_navigate,
on_hide=on_hide)
# Retry in case we're scrolling
sublime.set_timeout_async(lambda: self.ds_show_popup(content=content,
flags=flags,
location=location,
max_width=max_width,
max_height=max_height,
on_navigate=on_navigate,
on_hide=on_hide), repeat_duration_ms)
class ReplaceTextCommand(sublime_plugin.TextCommand):
"""Simple function to route text changes to view."""
def run(self, edit, a, b, result):
"""Replace given text for a region in a view."""
logger.debug("Replacing [%s] into region (%d, %d)" % (result, a, b))
self.view.replace(edit, sublime.Region(a, b), result)
class InsertTextCommand(sublime_plugin.TextCommand):
"""Simple function to route text inserts to view."""
def run(self, edit, a, result):
"""Insert given text for a region in a view."""
self.view.insert(edit, a, result)
class DevSkimAnalyzeCommand(sublime_plugin.TextCommand):
"""Perform an ad-hoc analysis of the open file."""
def run(self, text):
"""Execute the analysis."""
global devskim_event_listener
if not devskim_event_listener:
devskim_event_listener = DevSkimEventListener()
try:
devskim_event_listener.analyze_current_view(self.view)
except Exception as msg:
logger.warning("Error analyzing current view: %s" % msg)
class DevSkimReloadRulesCommand(sublime_plugin.TextCommand):
"""Mark the DevSkim rules to be reloaded next time they're needed."""
def run(self, text):
"""Execute the analysis."""
global rules, stylesheet_content
rules = []
stylesheet_content = ""
def plugin_loaded():
"""Handle the plugin_loaded event from ST3."""
logger.info('DevSkim plugin_loaded(), Sublime Text v%s' % sublime.version())
def plugin_unloaded():
"""Handle the plugin_unloaded event from ST3."""
logger.info("DevSkim plugin_unloaded()")

10
DevSkim.sublime-commands Normal file
Просмотреть файл

@ -0,0 +1,10 @@
[
{
"caption": "DevSkim: Analyze File",
"command": "dev_skim_analyze"
},
{
"caption": "DevSkim: Reload Configuration",
"command": "dev_skim_reload_rules"
}
]

2
DevSkim.sublime-project Normal file
Просмотреть файл

@ -0,0 +1,2 @@
{
}

53
DevSkim.sublime-settings Normal file
Просмотреть файл

@ -0,0 +1,53 @@
// Don't change any setting on this file. If you want to override the default
// behaviour, change the settings values on your
// Packages/User/DevSkim.sublime-settings file.
{
/* Enable verbose output to console */
"debug": false,
/* Don't analyze files greater than 512kb - set to -1 to disable */
"max_size": 524288,
/* Show all severities */
"show_severity": ["critical", "important", "moderate", "low", "defense-in-depth", "informational"],
/* Mark for the page gutter -- available options dot, circle, cross, or "" */
"gutter_mark": "dot",
/* Stylesheet for popup, from package directory */
"style": "css/dark.css",
/* Show only rules with the given tags, empty means "show all" */
"show_only_tags": [],
/* Sort results by either "severity" or "line_number" */
"sort_results_by": "line_number",
/* Run analysis as soon as a file is opened. */
"show_highlights_on_load": true,
/* Run analysis when the file is saved. */
"show_findings_on_save": true,
/* Run analysis whenever a file is changed. */
"show_highlights_on_modified": true,
/* Enable per-rule suppressions */
"allow_suppress_specific_rules": true,
/* Enable global suppressions */
"allow_suppress_all_rules": true,
/* Allow suppression for specific # of days, or -1 for "permanent" */
"suppress_days": [90, 365, -1],
/* List of rules files to also include. */
"custom_rules": [
],
/* Globally suppress these rules (list of rule IDs, like "DS123456") */
"suppress_rules": [
]
}

17
LICENSE.txt Normal file
Просмотреть файл

@ -0,0 +1,17 @@
Copyright (c) 2016 Microsoft Corporation
All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

35
Main.sublime-menu Normal file
Просмотреть файл

@ -0,0 +1,35 @@
[
{
"caption": "Preferences",
"mnemonic": "n",
"id": "preferences",
"children": [
{
"caption": "Package Settings",
"mnemonic": "P",
"id": "package-settings",
"children": [
{
"caption": "DevSkim",
"children": [
{
"command": "open_file",
"args": {
"file": "${packages}/DevSkim/DevSkim.sublime-settings"
},
"caption": "Settings – Default"
},
{
"command": "open_file",
"args": {
"file": "${packages}/User/DevSkim.sublime-settings"
},
"caption": "Settings – User"
}
]
}
]
}
]
}
]

121
README.md Normal file
Просмотреть файл

@ -0,0 +1,121 @@
DevSkim Plugin for Sublime Text
===============================
The plugin implements a security linter within the Sublime Text editor, leveraging the rules from the [DevSkim-Rules](https://github.com/Microsoft/DevSkim-Rules) repo. It helps software engineers to write secure code by flagging potentially dangerous calls, and gives in-context advice for remediation.
Requirements
--------------
The plugin requires Sublime Text 3 (build >= 3114), and will function on Windows, Linux, and MacOS.
Installation
------------
If using [Package Control](https://packagecontrol.io/) for Sublime Text, simply install the `DevSkim` package.
Alternatively, you can clone the plugin and rules repos directly into your Sublime plugin folder. For example, for Sublime Text 3 on a Mac this would look something like:
```
cd ~/"Library/Application Support/Sublime Text 3/Packages"
git clone --depth 1 https://github.com/Microsoft/DevSkim-Sublime-Plugin.git DevSkim
cd DevSkim
git clone --depth 1 https://github.com/Microsoft/DevSkim-Rules.git rules
```
And on Windows:
```
cd "%APPDATA%\Sublime Text 3\Packages"
git clone --depth 1 https://github.com/Microsoft/DevSkim-Sublime-Plugin.git DevSkim
cd DevSkim
git clone --depth 1 https://github.com/Microsoft/DevSkim-Rules.git rules
```
(`--depth 1` downloads only the current version to reduce the clone size.)
Note if you are using the portable version of Sublime Text, the location will be different. (See http://docs.sublimetext.info/en/latest/basic_concepts.html#the-data-directory for more info).
**IMPORTANT** If you already have a package called `DevSkim` installed, either remove this first, or clone this repo to a different folder, else module name resolution can break the plugin.
Platform support
----------------
#### Operating System:
The plugin has identical behavior across Windows, Mac, and Linux.
#### Sublime Text Version:
The plugin requires [Sublime Text 3](http://www.sublimetext.com/3) build >= 3114.
Features
--------
The below features are available via the keyboard shortcuts shown, or via the Command Palette (^ means the `ctrl` or `cmd` keys):
| Feature | Shortcut |
|-----------------------|-----------------|
| Run DevSkim | `^D` |
The
The "format on key" feature is on by default, which formats the current line after typing `;`, `}` or `enter`.
To disable it, go to `Preferences` -> `Package Settings` -> `DevSkim` -> `Plugin Settings - User`, and add
`"typescript_auto_format": false` to the json file.
Rules System
------------
The plugin supports both built-in and custom rules:
#### Built-In Rules
Built-in rules come from the [DevSkim-Rules](https://github.com/Microsoft/DevSkim-Rules.git) repo, and should be stored
in the `rules` directory within the DevSkim directory.
Rules are organized by subdirectory and file, but are flattened internally when loaded.
Each rule contains a set of patterns (strings and regular expressions) to match, a list of file types to
apply the rule to, and, optionally, a list of possible code fixes. An example rule is shown below:
```
[
{
"id": "DS126858",
"name": "Weak/Broken Hash Algorithm",
"active": true,
"tags": [
"Cryptography.BannedHashAlgorithm"
],
"severity": "critical",
"description": "A weak or broken hash algorithm was detected.",
"replacement": "Consider switching to use SHA-256 or SHA-512 instead.",
"rule_info": "https://github.com/microsoft/devskim/guidance/DS126858.md",
"applies_to": [
"$PHP"
],
"patterns": [
{
"pattern": "md5(",
"type": "string"
},
],
"fix_it": [
{
"type": "regex_substitute",
"name": "Change to SHA-256",
"search": "\\bmd5\\(([^\\)]+\\)",
"replace": "hash('sha256', \\1)"
}
]
}
]
```
Screenshots
------
TODO
Reporting Issues
-------
Please see [CONTRIBUTING](https://github.com/Microsoft/DevSkim-Sublime-Plugin/blob/master/CONTRIBUTING.md) for information on reporting issues and contributing code.
Tips and Known Issues
----
See tips and known issues in the [wiki page](https://github.com/Microsoft/DevSkim-Sublime-Plugin/wiki/Tips-and-Known-Issues).

28
css/dark.css Normal file
Просмотреть файл

@ -0,0 +1,28 @@
html {
background-color: #002b36;
color: #CCCCCC;
}
body {
}
a {
color: #268bd2;
}
b {
color: #b58900;
}
h1 {
color: #2aa198;
}
h2 {
color: #2aa198;
}
h3 {
}
h4 {
}
h5 {
color: #2aa198;
}
p {
margin-right: 10px;
}

29
css/default.css Normal file
Просмотреть файл

@ -0,0 +1,29 @@
html {
background-color: #ffffff;
color: #000000;
}
body {
font-size: 14px;
}
a {
color: #268bd2;
}
b {
color: #b58900;
}
h1 {
color: #2aa198;
font-size: 16px;
}
h2 {
color: #2aa198;
font-size: 14px;
}
h5 {
color: #2aa198;
font-size: 10px;
}
p {
margin-left: 10px;
margin-right: 10px;
}

4
messages.json Normal file
Просмотреть файл

@ -0,0 +1,4 @@
{
"install": "messages/install.txt",
"1.0.0": "messages/1.0.0.txt"
}

5
messages/1.0.0.txt Normal file
Просмотреть файл

@ -0,0 +1,5 @@
DevSkim 1.0.0
==============
2016-09-21 - Initial public release of the DevSkim Sublime Text plugin.

22
messages/install.txt Normal file
Просмотреть файл

@ -0,0 +1,22 @@
Welcome to DevSkim
=====================
DevSkim is a source code linter, specifically designed to flag high-risk
security vulnerabilities, provide actionable recommendations (including
"auto-fix" functionality) and time-limited suppressions.
DevSkim comes with a pre-defined set of rules, which can be extended by
users or organizations.
Issues, Questions or Bugs?
--------------------------
For issues with DevSkim, please open an issue for the plugin project:
https://github.com/Microsoft/DevSkim-SublimeText/issues
For issues with rules, please open an issue for the rules project:
https://github.com/Microsoft/DevSkim-Rules/issues