refactor ZirconCode's Agent to only run when ENABLE_INSECURE_AGENTS is enabled in .env

Andrew Cantino %!s(int64=11) %!d(string=hace) años
padre
commit
a67231f678

+ 4 - 0
.env.example

@@ -86,6 +86,10 @@ AWS_SANDBOX=false
86 86
 # You should not allow this on a shared Huginn box because it is not secure.
87 87
 ALLOW_JSONPATH_EVAL=false
88 88
 
89
+# Enable this setting to allow insecure Agents like the ShellCommandAgent.  Only do this
90
+# when you trust everyone using your Huginn installation.
91
+ENABLE_INSECURE_AGENTS=false
92
+
89 93
 # Use Graphviz for generating diagrams instead of using Google Chart
90 94
 # Tools.  Specify a dot(1) command path built with SVG support
91 95
 # enabled.

+ 0 - 101
app/models/agents/command_agent.rb

@@ -1,101 +0,0 @@
1
-module Agents
2
-  class CommandAgent < Agent
3
-    
4
-    require 'open3'
5
-
6
-
7
-    default_schedule "midnight"
8
-
9
-
10
-    description <<-MD
11
-
12
-      The CommandAgent can execute commands on your local system, returning the output.
13
-
14
-      `command` specifies the command to be executed, and `path` will tell CommandAgent in what directory to run this command.
15
-
16
-      `expected_update_period_in_days` is used to determine if the Agent is working.
17
-
18
-      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.
19
-
20
-      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.
21
-
22
-      *Warning*: Misuse of this Agent can pose a security threat.
23
-
24
-    MD
25
-
26
-    event_description <<-MD
27
-    Events look like this:
28
-
29
-      {
30
-        'command' => 'pwd',
31
-        'path' => '/home/Huginn',
32
-        'exit_status' => '0',
33
-        'errors' => '',
34
-        'output' => '/home/Huginn' 
35
-      }
36
-    MD
37
-
38
-    def default_options
39
-      {
40
-          'path' => "/home",
41
-          'command' => "pwd",
42
-          'expected_update_period_in_days' => 1
43
-      }
44
-    end
45
-
46
-    def validate_options
47
-      unless options['path'].present? && options['command'].present? && options['expected_update_period_in_days'].present?
48
-        errors.add(:base, "The path, command, and expected_update_period_in_days fields are all required.")
49
-      end   
50
-      unless File.directory?(options['path'])
51
-        errors.add(:base, "#{options['path']} is not a real directory.")
52
-      end
53
-    end
54
-
55
-    def working?
56
-      event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs?
57
-    end
58
-
59
-    def exec_command(opts = options)
60
-      command = opts['command'] || options['command']
61
-      path = opts['path'] || options['path']
62
-
63
-      result = nil
64
-      errors = nil
65
-      exit_status = nil
66
-
67
-      Dir.chdir(path){
68
-        begin
69
-          stdin, stdout, stderr, wait_thr = Open3.popen3(command)
70
-          exit_status = wait_thr.value.to_i
71
-          result = stdout.gets(nil)
72
-          errors = stderr.gets(nil)
73
-        rescue Exception => e
74
-          errors = e.to_s
75
-        end
76
-      }
77
-
78
-      result.chomp! if result.is_a?(String)
79
-      result = '' if result.nil?
80
-      errors = '' if errors.nil?
81
-
82
-      vals = {"command" => command, "path" => path, "exit_status" => exit_status, "errors" => errors, "output" => result}
83
-      evnt = create_event :payload => vals
84
-
85
-      log("Ran '#{command}' under '#{path}'", :outbound_event => evnt)
86
-    end
87
-
88
-
89
-    def receive(incoming_events)
90
-      incoming_events.each do |event|
91
-        exec_command(event.payload)
92
-      end
93
-    end
94
-
95
-    def check
96
-      exec_command(options)
97
-    end
98
-
99
-
100
-  end
101
-end

+ 111 - 0
app/models/agents/shell_command_agent.rb

@@ -0,0 +1,111 @@
1
+require 'open3'
2
+
3
+module Agents
4
+  class ShellCommandAgent < Agent
5
+    default_schedule "never"
6
+
7
+    def self.should_run?
8
+      ENV['ENABLE_INSECURE_AGENTS'] == "true"
9
+    end
10
+
11
+    description <<-MD
12
+      The ShellCommandAgent can execute commands on your local system, returning the output.
13
+
14
+      `command` specifies the command to be executed, and `path` will tell ShellCommandAgent in what directory to run this command.
15
+
16
+      `expected_update_period_in_days` is used to determine if the Agent is working.
17
+
18
+      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.
19
+
20
+      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.
21
+
22
+      *Warning*: This type of Agent runs arbitrary commands on your system, #{Agents::ShellCommandAgent.should_run? ? "but is **currently enabled**" : "and is **currently disabled**"}.
23
+      Only enable this Agent if you trust everyone using your Huginn installation.
24
+      You can enable this Agent in your .env file by setting `ENABLE_INSECURE_AGENTS` to `true`.
25
+    MD
26
+
27
+    event_description <<-MD
28
+    Events look like this:
29
+
30
+      {
31
+        'command' => 'pwd',
32
+        'path' => '/home/Huginn',
33
+        'exit_status' => '0',
34
+        'errors' => '',
35
+        'output' => '/home/Huginn' 
36
+      }
37
+    MD
38
+
39
+    def default_options
40
+      {
41
+          'path' => "/",
42
+          'command' => "pwd",
43
+          'expected_update_period_in_days' => 1
44
+      }
45
+    end
46
+
47
+    def validate_options
48
+      unless options['path'].present? && options['command'].present? && options['expected_update_period_in_days'].present?
49
+        errors.add(:base, "The path, command, and expected_update_period_in_days fields are all required.")
50
+      end
51
+
52
+      unless File.directory?(options['path'])
53
+        errors.add(:base, "#{options['path']} is not a real directory.")
54
+      end
55
+    end
56
+
57
+    def working?
58
+      Agents::ShellCommandAgent.should_run? && event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs?
59
+    end
60
+
61
+    def receive(incoming_events)
62
+      incoming_events.each do |event|
63
+        handle(event.payload, event)
64
+      end
65
+    end
66
+
67
+    def check
68
+      handle(options)
69
+    end
70
+
71
+    private
72
+
73
+    def handle(opts = options, event = nil)
74
+      if Agents::ShellCommandAgent.should_run?
75
+        command = opts['command'] || options['command']
76
+        path = opts['path'] || options['path']
77
+
78
+        result, errors, exit_status = run_command(path, command)
79
+
80
+        vals = {"command" => command, "path" => path, "exit_status" => exit_status, "errors" => errors, "output" => result}
81
+        created_event = create_event :payload => vals
82
+
83
+        log("Ran '#{command}' under '#{path}'", :outbound_event => created_event, :inbound_event => event)
84
+      else
85
+        log("Unable to run because insecure agents are not enabled.  Edit ENABLE_INSECURE_AGENTS in the Huginn .env configuration.")
86
+      end
87
+    end
88
+
89
+    def run_command(path, command)
90
+      result = nil
91
+      errors = nil
92
+      exit_status = nil
93
+
94
+      Dir.chdir(path){
95
+        begin
96
+          stdin, stdout, stderr, wait_thr = Open3.popen3(command)
97
+          exit_status = wait_thr.value.to_i
98
+          result = stdout.gets(nil)
99
+          errors = stderr.gets(nil)
100
+        rescue Exception => e
101
+          errors = e.to_s
102
+        end
103
+      }
104
+
105
+      result = result.to_s.strip
106
+      errors = errors.to_s.strip
107
+
108
+      [result, errors, exit_status]
109
+    end
110
+  end
111
+end

+ 0 - 70
spec/models/agents/command_agent_spec.rb

@@ -1,70 +0,0 @@
1
-require 'spec_helper'
2
-
3
-describe Agents::CommandAgent do
4
-
5
-  before do
6
-    @valid_path = Dir.pwd
7
-    @valid_params = {
8
-        :path  => @valid_path,
9
-        :command  => "pwd",
10
-        :expected_update_period_in_days => "1",
11
-      }
12
-
13
-    @checker = Agents::CommandAgent.new(:name => "somename", :options => @valid_params)
14
-    @checker.user = users(:jane)
15
-    @checker.save!
16
-
17
-    @event = Event.new
18
-    @event.agent = agents(:jane_weather_agent)
19
-    @event.payload = {
20
-      :command => "pwd"
21
-    }
22
-    @event.save!
23
-  end
24
-
25
-  describe "validation" do
26
-    before do
27
-      @checker.should be_valid
28
-    end
29
-
30
-    it "should validate presence of necessary fields" do
31
-      @checker.options[:command] = nil
32
-      @checker.should_not be_valid
33
-    end
34
-
35
-    it "should validate path" do
36
-      @checker.options[:path] = 'notarealpath/itreallyisnt'
37
-      @checker.should_not be_valid
38
-    end
39
-  end
40
-
41
-  describe "#working?" do
42
-    it "checks if its generating events as scheduled" do
43
-      @checker.should_not be_working
44
-      @checker.check
45
-      @checker.reload.should be_working
46
-      three_days_from_now = 3.days.from_now
47
-      stub(Time).now { three_days_from_now }
48
-      @checker.should_not be_working
49
-    end
50
-  end
51
-
52
-  describe "#check" do
53
-    it "should check that initial run creates an event" do
54
-      expect { @checker.check }.to change { Event.count }.by(1)
55
-    end
56
-  end
57
-
58
-  describe "#receive" do
59
-    it "checks if creates events" do
60
-      @checker.receive([@event])
61
-      Event.last.payload[:path].should == @valid_path
62
-    end
63
-    it "checks if options are taken from event" do
64
-      @event.payload[:command] = 'notarealcommand'
65
-      @checker.receive([@event])
66
-      Event.last.payload[:command].should == 'notarealcommand'
67
-    end
68
-  end
69
-
70
-end

+ 99 - 0
spec/models/agents/shell_command_agent_spec.rb

@@ -0,0 +1,99 @@
1
+require 'spec_helper'
2
+
3
+describe Agents::ShellCommandAgent do
4
+  before do
5
+    @valid_path = Dir.pwd
6
+
7
+    @valid_params = {
8
+        :path  => @valid_path,
9
+        :command  => "pwd",
10
+        :expected_update_period_in_days => "1",
11
+      }
12
+
13
+    @checker = Agents::ShellCommandAgent.new(:name => "somename", :options => @valid_params)
14
+    @checker.user = users(:jane)
15
+    @checker.save!
16
+
17
+    @event = Event.new
18
+    @event.agent = agents(:jane_weather_agent)
19
+    @event.payload = {
20
+      :command => "ls"
21
+    }
22
+    @event.save!
23
+
24
+    stub(Agents::ShellCommandAgent).should_run? { true }
25
+  end
26
+
27
+  describe "validation" do
28
+    before do
29
+      @checker.should be_valid
30
+    end
31
+
32
+    it "should validate presence of necessary fields" do
33
+      @checker.options[:command] = nil
34
+      @checker.should_not be_valid
35
+    end
36
+
37
+    it "should validate path" do
38
+      @checker.options[:path] = 'notarealpath/itreallyisnt'
39
+      @checker.should_not be_valid
40
+    end
41
+
42
+    it "should validate path" do
43
+      @checker.options[:path] = '/'
44
+      @checker.should be_valid
45
+    end
46
+  end
47
+
48
+  describe "#working?" do
49
+    it "generating events as scheduled" do
50
+      stub(@checker).run_command(@valid_path, 'pwd') { ["fake pwd output", "", 0] }
51
+
52
+      @checker.should_not be_working
53
+      @checker.check
54
+      @checker.reload.should be_working
55
+      three_days_from_now = 3.days.from_now
56
+      stub(Time).now { three_days_from_now }
57
+      @checker.should_not be_working
58
+    end
59
+  end
60
+
61
+  describe "#check" do
62
+    before do
63
+      stub(@checker).run_command(@valid_path, 'pwd') { ["fake pwd output", "", 0] }
64
+    end
65
+
66
+    it "should create an event when checking" do
67
+      expect { @checker.check }.to change { Event.count }.by(1)
68
+      Event.last.payload[:path].should == @valid_path
69
+      Event.last.payload[:command].should == 'pwd'
70
+      Event.last.payload[:output].should == "fake pwd output"
71
+    end
72
+
73
+    it "does not run when should_run? is false" do
74
+      stub(Agents::ShellCommandAgent).should_run? { false }
75
+      expect { @checker.check }.not_to change { Event.count }
76
+    end
77
+  end
78
+
79
+  describe "#receive" do
80
+    before do
81
+      stub(@checker).run_command(@valid_path, @event.payload[:command]) { ["fake ls output", "", 0] }
82
+    end
83
+
84
+    it "creates events" do
85
+      @checker.receive([@event])
86
+      Event.last.payload[:path].should == @valid_path
87
+      Event.last.payload[:command].should == @event.payload[:command]
88
+      Event.last.payload[:output].should == "fake ls output"
89
+    end
90
+
91
+    it "does not run when should_run? is false" do
92
+      stub(Agents::ShellCommandAgent).should_run? { false }
93
+
94
+      expect {
95
+        @checker.receive([@event])
96
+      }.not_to change { Event.count }
97
+    end
98
+  end
99
+end