Module: PWN::Cron

Defined in:
lib/pwn/cron.rb

Overview

PWN::Cron provides cron / scheduled task management for the pwn-ai agent (equivalent to Hermes cron jobs + scheduler). Jobs are defined in ~/.pwn/cron/jobs.yml and can be triggered by system cron, manual run, or from within pwn-ai agent loops.

Each job can contain a prompt (for pwn-ai), a ruby script snippet, or reference to external script. Delivery can be ‘log’ (default), ‘email’, etc. (email would require additional plugins).

Constant Summary collapse

CRON_DIR =
File.join(Dir.home, '.pwn', 'cron')
JOBS_FILE =
File.join(CRON_DIR, 'jobs.yml')

Class Method Summary collapse

Class Method Details

.authorsObject

Author(s)

0day Inc. <support@0dayinc.com>



190
191
192
# File 'lib/pwn/cron.rb', line 190

public_class_method def self.authors
  "AUTHOR(S):\n  0day Inc. <support@0dayinc.com>\n"
end

.create(opts = {}) ⇒ Object

Supported Method Parameters

job = PWN::Cron.create(

name: 'optional',
schedule: 'required e.g. "0 * * * *" or "30m" or "every 2h"',
prompt: 'optional - pwn-ai prompt to run',
ruby: 'optional - ruby snippet to eval',
script: 'optional - path to external script',
delivery: 'log|stdout (default log)',
enabled: true

)



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/pwn/cron.rb', line 44

public_class_method def self.create(opts = {})
  jobs = load_jobs
  id = SecureRandom.hex(6)
  name = opts[:name] || "job-#{id}"
  job = {
    id: id,
    name: name,
    schedule: opts[:schedule] || '0 * * * *',
    prompt: opts[:prompt],
    ruby: opts[:ruby],
    script: opts[:script],
    delivery: opts[:delivery] || 'log',
    enabled: opts.fetch(:enabled, true),
    created_at: Time.now.utc.iso8601,
    last_run: nil,
    last_status: nil
  }
  jobs[id] = job
  save_jobs(jobs)

  # Optionally install a crontab entry (user must have permission)
  install_crontab_entry(job) if opts[:install_crontab]

  job
end

.cron_dirObject

Supported Method Parameters

dir = PWN::Cron.cron_dir



23
24
25
26
# File 'lib/pwn/cron.rb', line 23

public_class_method def self.cron_dir
  FileUtils.mkdir_p(CRON_DIR)
  CRON_DIR
end

.disable(opts = {}) ⇒ Object



149
150
151
# File 'lib/pwn/cron.rb', line 149

public_class_method def self.disable(opts = {})
  toggle(opts[:id], false)
end

.enable(opts = {}) ⇒ Object

Supported Method Parameters

PWN::Cron.enable/disable(id:)



145
146
147
# File 'lib/pwn/cron.rb', line 145

public_class_method def self.enable(opts = {})
  toggle(opts[:id], true)
end

.helpObject

Display Usage for this Module



195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/pwn/cron.rb', line 195

public_class_method def self.help
  puts <<~USAGE
    USAGE:
      PWN::Cron.create(schedule: '0 * * * *', prompt: 'Run daily recon on target.com using NmapIt and report', name: 'daily-recon')
      PWN::Cron.list
      res = PWN::Cron.run(id: 'abc123')
      PWN::Cron.enable(id: 'abc123')
      PWN::Cron.disable(id: 'abc123')
      PWN::Cron.remove(id: 'abc123')
      # To have system cron call it, use install_crontab_entry or the :install_crontab option on create

      #{self}.authors
  USAGE
end

.install_crontab_entry(job) ⇒ Object

Install a crontab line that invokes this job via pwn (assumes /opt/pwn and rvm ruby-4.0.1@pwn - user can edit crontab)



155
156
157
158
159
160
161
162
163
164
# File 'lib/pwn/cron.rb', line 155

public_class_method def self.install_crontab_entry(job)
  cron_line = "#{job[:schedule]} cd /opt/pwn && /usr/local/rvm/bin/rvm ruby-4.0.1@pwn do ruby -I lib -e 'require \"pwn\"; PWN::Cron.run(id: \"#{job[:id]}\")' >> #{File.join(cron_dir, 'cron.log')} 2>&1"
  # Append to user's crontab (non-destructive)
  existing = `crontab -l 2>/dev/null || true`
  unless existing.include?(job[:id])
    new_cron = existing + "\n# pwn-cron #{job[:name]} (#{job[:id]})\n#{cron_line}\n"
    IO.popen('crontab -', 'w') { |io| io.write(new_cron) }
  end
  cron_line
end

.listObject

Supported Method Parameters

jobs = PWN::Cron.list



30
31
32
# File 'lib/pwn/cron.rb', line 30

public_class_method def self.list
  load_jobs
end

.remove(opts = {}) ⇒ Object

rubocop:disable Naming/PredicateMethod



135
136
137
138
139
140
141
# File 'lib/pwn/cron.rb', line 135

public_class_method def self.remove(opts = {}) # rubocop:disable Naming/PredicateMethod
  id = opts[:id]
  jobs = load_jobs
  jobs.delete(id)
  save_jobs(jobs)
  true
end

.run(opts = {}) ⇒ Object

Supported Method Parameters

PWN::Cron.run(id: ‘required or name’) Executes the job (for pwn-ai prompt it will use current active AI engine via PWN::AI::* but without full REPL hook unless in pwn-ai).



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/pwn/cron.rb', line 74

public_class_method def self.run(opts = {})
  id = opts[:id]
  jobs = load_jobs
  job = jobs[id] || jobs.values.find { |j| j[:name] == id || j[:id] == id }
  raise "Job #{id} not found" unless job

  start = Time.now
  result = nil
  status = 'success'

  begin
    if job[:prompt]
      engine = begin
        PWN::Env[:ai][:active].to_s.downcase.to_sym
      rescue StandardError
        :grok
      end
      case engine
      when :grok
        result = PWN::AI::Grok.chat(request: job[:prompt], spinner: false)
      when :ollama
        result = PWN::AI::Ollama.chat(request: job[:prompt], spinner: false)
      when :openai
        result = PWN::AI::OpenAI.chat(request: job[:prompt], spinner: false)
      when :anthropic
        result = PWN::AI::Anthropic.chat(request: job[:prompt], spinner: false)
      end
      result = begin
        result[:choices].last[:content]
      rescue StandardError
        result.to_s
      end
    elsif job[:ruby]
      result = eval(job[:ruby], TOPLEVEL_BINDING) # rubocop:disable Security/Eval
    elsif job[:script] && File.exist?(job[:script])
      result = `#{job[:script]} 2>&1`
    else
      result = 'No prompt/ruby/script defined'
    end

    if job[:delivery] == 'log'
      log_path = File.join(cron_dir, "#{job[:id]}.log")
      File.open(log_path, 'a') do |f|
        f.puts("[#{Time.now}] RUN #{job[:name]} (#{job[:id]})\n#{result}\n---")
      end
    end
  rescue StandardError => e
    status = 'error'
    result = "ERROR: #{e.class} - #{e.message}\n#{e.backtrace.first(5).join("\n")}"
  end

  job[:last_run] = Time.now.utc.iso8601
  job[:last_status] = status
  jobs[job[:id]] = job
  save_jobs(jobs)

  { job: job, result: result, duration: Time.now - start, status: status }
end