From a67231f6784279f17539b47b4547127dd33b7d10 Mon Sep 17 00:00:00 2001 From: Andrew Cantino Date: Sat, 19 Apr 2014 13:44:07 -0700 Subject: [PATCH] refactor ZirconCode's Agent to only run when ENABLE_INSECURE_AGENTS is enabled in .env --- .env.example | 4 + app/models/agents/command_agent.rb | 101 ---------------- app/models/agents/shell_command_agent.rb | 111 ++++++++++++++++++ spec/models/agents/command_agent_spec.rb | 70 ----------- .../models/agents/shell_command_agent_spec.rb | 99 ++++++++++++++++ 5 files changed, 214 insertions(+), 171 deletions(-) delete mode 100644 app/models/agents/command_agent.rb create mode 100644 app/models/agents/shell_command_agent.rb delete mode 100644 spec/models/agents/command_agent_spec.rb create mode 100644 spec/models/agents/shell_command_agent_spec.rb diff --git a/.env.example b/.env.example index 918b674f6e..ef85ae6596 100644 --- a/.env.example +++ b/.env.example @@ -86,6 +86,10 @@ AWS_SANDBOX=false # You should not allow this on a shared Huginn box because it is not secure. ALLOW_JSONPATH_EVAL=false +# Enable this setting to allow insecure Agents like the ShellCommandAgent. Only do this +# when you trust everyone using your Huginn installation. +ENABLE_INSECURE_AGENTS=false + # Use Graphviz for generating diagrams instead of using Google Chart # Tools. Specify a dot(1) command path built with SVG support # enabled. diff --git a/app/models/agents/command_agent.rb b/app/models/agents/command_agent.rb deleted file mode 100644 index a62b61eefd..0000000000 --- a/app/models/agents/command_agent.rb +++ /dev/null @@ -1,101 +0,0 @@ -module Agents - class CommandAgent < Agent - - require 'open3' - - - default_schedule "midnight" - - - description <<-MD - - The CommandAgent can execute commands on your local system, returning the output. - - `command` specifies the command to be executed, and `path` will tell CommandAgent in what directory to run this command. - - `expected_update_period_in_days` is used to determine if the Agent is working. - - CommandAgent can also act upon recieveing events. These events may contain their own path and command arguments. If they do not, CommandAgent will use the configured options. For this reason, please specify defaults even if you are planning to have this Agent respond to events. - - The resulting event will contain the `command` which was executed, the `path` it was executed under, the `exit_status` of the command, the `errors`, and the actual `output`. CommandAgent will not log an error if the result implies that something went wrong. - - *Warning*: Misuse of this Agent can pose a security threat. - - MD - - event_description <<-MD - Events look like this: - - { - 'command' => 'pwd', - 'path' => '/home/Huginn', - 'exit_status' => '0', - 'errors' => '', - 'output' => '/home/Huginn' - } - MD - - def default_options - { - 'path' => "/home", - 'command' => "pwd", - 'expected_update_period_in_days' => 1 - } - end - - def validate_options - unless options['path'].present? && options['command'].present? && options['expected_update_period_in_days'].present? - errors.add(:base, "The path, command, and expected_update_period_in_days fields are all required.") - end - unless File.directory?(options['path']) - errors.add(:base, "#{options['path']} is not a real directory.") - end - end - - def working? - event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? - end - - def exec_command(opts = options) - command = opts['command'] || options['command'] - path = opts['path'] || options['path'] - - result = nil - errors = nil - exit_status = nil - - Dir.chdir(path){ - begin - stdin, stdout, stderr, wait_thr = Open3.popen3(command) - exit_status = wait_thr.value.to_i - result = stdout.gets(nil) - errors = stderr.gets(nil) - rescue Exception => e - errors = e.to_s - end - } - - result.chomp! if result.is_a?(String) - result = '' if result.nil? - errors = '' if errors.nil? - - vals = {"command" => command, "path" => path, "exit_status" => exit_status, "errors" => errors, "output" => result} - evnt = create_event :payload => vals - - log("Ran '#{command}' under '#{path}'", :outbound_event => evnt) - end - - - def receive(incoming_events) - incoming_events.each do |event| - exec_command(event.payload) - end - end - - def check - exec_command(options) - end - - - end -end diff --git a/app/models/agents/shell_command_agent.rb b/app/models/agents/shell_command_agent.rb new file mode 100644 index 0000000000..3570dfea68 --- /dev/null +++ b/app/models/agents/shell_command_agent.rb @@ -0,0 +1,111 @@ +require 'open3' + +module Agents + class ShellCommandAgent < Agent + default_schedule "never" + + def self.should_run? + ENV['ENABLE_INSECURE_AGENTS'] == "true" + end + + description <<-MD + The ShellCommandAgent can execute commands on your local system, returning the output. + + `command` specifies the command to be executed, and `path` will tell ShellCommandAgent in what directory to run this command. + + `expected_update_period_in_days` is used to determine if the Agent is working. + + ShellCommandAgent can also act upon received events. These events may contain their own `path` and `command` values. If they do not, ShellCommandAgent will use the configured options. For this reason, please specify defaults even if you are planning to have this Agent to respond to events. + + The resulting event will contain the `command` which was executed, the `path` it was executed under, the `exit_status` of the command, the `errors`, and the actual `output`. ShellCommandAgent will not log an error if the result implies that something went wrong. + + *Warning*: This type of Agent runs arbitrary commands on your system, #{Agents::ShellCommandAgent.should_run? ? "but is **currently enabled**" : "and is **currently disabled**"}. + Only enable this Agent if you trust everyone using your Huginn installation. + You can enable this Agent in your .env file by setting `ENABLE_INSECURE_AGENTS` to `true`. + MD + + event_description <<-MD + Events look like this: + + { + 'command' => 'pwd', + 'path' => '/home/Huginn', + 'exit_status' => '0', + 'errors' => '', + 'output' => '/home/Huginn' + } + MD + + def default_options + { + 'path' => "/", + 'command' => "pwd", + 'expected_update_period_in_days' => 1 + } + end + + def validate_options + unless options['path'].present? && options['command'].present? && options['expected_update_period_in_days'].present? + errors.add(:base, "The path, command, and expected_update_period_in_days fields are all required.") + end + + unless File.directory?(options['path']) + errors.add(:base, "#{options['path']} is not a real directory.") + end + end + + def working? + Agents::ShellCommandAgent.should_run? && event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs? + end + + def receive(incoming_events) + incoming_events.each do |event| + handle(event.payload, event) + end + end + + def check + handle(options) + end + + private + + def handle(opts = options, event = nil) + if Agents::ShellCommandAgent.should_run? + command = opts['command'] || options['command'] + path = opts['path'] || options['path'] + + result, errors, exit_status = run_command(path, command) + + vals = {"command" => command, "path" => path, "exit_status" => exit_status, "errors" => errors, "output" => result} + created_event = create_event :payload => vals + + log("Ran '#{command}' under '#{path}'", :outbound_event => created_event, :inbound_event => event) + else + log("Unable to run because insecure agents are not enabled. Edit ENABLE_INSECURE_AGENTS in the Huginn .env configuration.") + end + end + + def run_command(path, command) + result = nil + errors = nil + exit_status = nil + + Dir.chdir(path){ + begin + stdin, stdout, stderr, wait_thr = Open3.popen3(command) + exit_status = wait_thr.value.to_i + result = stdout.gets(nil) + errors = stderr.gets(nil) + rescue Exception => e + errors = e.to_s + end + } + + result = result.to_s.strip + errors = errors.to_s.strip + + [result, errors, exit_status] + end + end +end \ No newline at end of file diff --git a/spec/models/agents/command_agent_spec.rb b/spec/models/agents/command_agent_spec.rb deleted file mode 100644 index ad2713b68d..0000000000 --- a/spec/models/agents/command_agent_spec.rb +++ /dev/null @@ -1,70 +0,0 @@ -require 'spec_helper' - -describe Agents::CommandAgent do - - before do - @valid_path = Dir.pwd - @valid_params = { - :path => @valid_path, - :command => "pwd", - :expected_update_period_in_days => "1", - } - - @checker = Agents::CommandAgent.new(:name => "somename", :options => @valid_params) - @checker.user = users(:jane) - @checker.save! - - @event = Event.new - @event.agent = agents(:jane_weather_agent) - @event.payload = { - :command => "pwd" - } - @event.save! - end - - describe "validation" do - before do - @checker.should be_valid - end - - it "should validate presence of necessary fields" do - @checker.options[:command] = nil - @checker.should_not be_valid - end - - it "should validate path" do - @checker.options[:path] = 'notarealpath/itreallyisnt' - @checker.should_not be_valid - end - end - - describe "#working?" do - it "checks if its generating events as scheduled" do - @checker.should_not be_working - @checker.check - @checker.reload.should be_working - three_days_from_now = 3.days.from_now - stub(Time).now { three_days_from_now } - @checker.should_not be_working - end - end - - describe "#check" do - it "should check that initial run creates an event" do - expect { @checker.check }.to change { Event.count }.by(1) - end - end - - describe "#receive" do - it "checks if creates events" do - @checker.receive([@event]) - Event.last.payload[:path].should == @valid_path - end - it "checks if options are taken from event" do - @event.payload[:command] = 'notarealcommand' - @checker.receive([@event]) - Event.last.payload[:command].should == 'notarealcommand' - end - end - -end \ No newline at end of file diff --git a/spec/models/agents/shell_command_agent_spec.rb b/spec/models/agents/shell_command_agent_spec.rb new file mode 100644 index 0000000000..ee10362ad6 --- /dev/null +++ b/spec/models/agents/shell_command_agent_spec.rb @@ -0,0 +1,99 @@ +require 'spec_helper' + +describe Agents::ShellCommandAgent do + before do + @valid_path = Dir.pwd + + @valid_params = { + :path => @valid_path, + :command => "pwd", + :expected_update_period_in_days => "1", + } + + @checker = Agents::ShellCommandAgent.new(:name => "somename", :options => @valid_params) + @checker.user = users(:jane) + @checker.save! + + @event = Event.new + @event.agent = agents(:jane_weather_agent) + @event.payload = { + :command => "ls" + } + @event.save! + + stub(Agents::ShellCommandAgent).should_run? { true } + end + + describe "validation" do + before do + @checker.should be_valid + end + + it "should validate presence of necessary fields" do + @checker.options[:command] = nil + @checker.should_not be_valid + end + + it "should validate path" do + @checker.options[:path] = 'notarealpath/itreallyisnt' + @checker.should_not be_valid + end + + it "should validate path" do + @checker.options[:path] = '/' + @checker.should be_valid + end + end + + describe "#working?" do + it "generating events as scheduled" do + stub(@checker).run_command(@valid_path, 'pwd') { ["fake pwd output", "", 0] } + + @checker.should_not be_working + @checker.check + @checker.reload.should be_working + three_days_from_now = 3.days.from_now + stub(Time).now { three_days_from_now } + @checker.should_not be_working + end + end + + describe "#check" do + before do + stub(@checker).run_command(@valid_path, 'pwd') { ["fake pwd output", "", 0] } + end + + it "should create an event when checking" do + expect { @checker.check }.to change { Event.count }.by(1) + Event.last.payload[:path].should == @valid_path + Event.last.payload[:command].should == 'pwd' + Event.last.payload[:output].should == "fake pwd output" + end + + it "does not run when should_run? is false" do + stub(Agents::ShellCommandAgent).should_run? { false } + expect { @checker.check }.not_to change { Event.count } + end + end + + describe "#receive" do + before do + stub(@checker).run_command(@valid_path, @event.payload[:command]) { ["fake ls output", "", 0] } + end + + it "creates events" do + @checker.receive([@event]) + Event.last.payload[:path].should == @valid_path + Event.last.payload[:command].should == @event.payload[:command] + Event.last.payload[:output].should == "fake ls output" + end + + it "does not run when should_run? is false" do + stub(Agents::ShellCommandAgent).should_run? { false } + + expect { + @checker.receive([@event]) + }.not_to change { Event.count } + end + end +end