Reed G. Law
Reed G. Law

Reputation: 3945

What are the first steps in hardening a legacy Rails application?

There is a production system that has been running for many years, first as a PHP application, then as a hybrid with Rails, and now completely in Rails. It's unclear how long it has been around. The oldest git commit is from 5 years ago.

The goal is to keep the system running at all costs. It doesn't matter what code we use as long as nothing breaks. Currently it's at Rails version 3.2.33.

If we don't upgrade any gems we run the chance of becoming obsolete and undeployable. If we upgrade we will need to make changes to the code causing potential bugs to creep in. Not only do we face code rot, but also downtime due to AWS outages.

What would be the first step to make sure nothing breaks? I've spent months writing cucumber (integration) tests but it's hard to cover every edge case. The app has been running so long that most bugs have been fixed and there are few new exceptions. Testing was not a priority from the beginning so most of the code is undocumented.

Upvotes: 1

Views: 301

Answers (3)

David A. Wheeler
David A. Wheeler

Reputation: 561

Writing many tests is a key first step, glad to see that. Check your statement coverage to see if you're missing tests of important areas of code, and be sure that you have integration tests that cover the key sequences. The idea is to modify your tests to reduce the risk that a change will cause a user-visible failure.

Now setup continuous testing on some system.

Set your Gemfile and .ruby-version so that you have specific control over exactly what versions of everything gets loaded. That doesn't automatically update - but it ensures that you have control over what you update. Check in both Gemfile and Gemfile.lock.

At this point you can slowly increase version numbers. Don't jump lots of version numbers - it's typically better to upgrade slowly so that you can see deprecation warnings. Fix those, rinse, repeat.

Modify your (input) validators to be picky whitelists ("it must be of this form or I won't accept it"). If you can prevent bad data from entering your system, it's more likely to work correctly and will typically be harder to attack.

For security, consider adding secureheaders, and set CSP as strong as you can stand.

Start adding some static analyzers. Rubocop and Brakeman are very useful. You'll probably have to configure Rubocop to only complain about a few things, and then slowly increase what they report. Add all your checks to the default "rake" command, so that you can just type "rake" to run static analyzers and the test suite.

There's no magic, regardless of what framework you use. People make mistakes, and pretending otherwise isn't helpful.

You might the CII Best Practices badge project a useful example. It uses RoR, and I lead that. In particular, see: * CONTRIBUTING * Security information (assurance case)

From CONTRIBUTING: "In general we try to be proactive to detect and eliminate mistakes and vulnerabilities as soon as possible, and to reduce their impact when they do happen. We use a defensive design and coding style to reduce the likelihood of mistakes, a variety of tools that try to detect mistakes early, and an automatic test suite with significant coverage."

Upvotes: 1

Michael Durrant
Michael Durrant

Reputation: 96484

The first step would be to put specific gem version for all gems used in the gem file.

For exmaple

 gem 'rspec-rails'

might become

 gem 'rspec-rails', '2.14.1'

You can figure out which versions are currently being used by looking at your Gemfile.lock, for example, this line in Gemfile.lock shows the version selected for rspec:

rspec (2.14.1)

and even if the Gemfile has no version, e.g.

gem chronic

the Gemfile will have the version used, e.g.

chronic (0.10.2)

If we don't upgrade any gems we run the chance of becoming obsolete and undeployable. If we upgrade we will need to make changes to the code causing potential bugs to creep in.

Yes, that's your dilemma. There isn't any magic, you have to pick which of these two priorities you want to address. As aNoble indicates, RoR is not a framework that can 'stay in place'. constant change of the gems that combine to make up most applications means that RoR applications do not age well.

You should explain and repeat, repeat, repeat this to the project owner. Frequently this is the kind of principle that is "accepted" - but not really - as the same questions continue to be asked "despite that, how can I upgrade it, how can I make sure nothing changes or breaks, etc."

If the application will be retired in a couple of months, no problem. If there is no real plan to sunset the application and it will continue to be an important part of the business then you just have to use resources to maintain it. no free lunch in RoR land.

Upvotes: 1

aNoble
aNoble

Reputation: 7072

Honestly, I find that Ruby on Rails is not ideal for this sort of application. Both Ruby and Rails have a very aggressive release schedule, and Rails especially isn't afraid to ditch backwards compatibility. Rails is great for agile development where things are always changing, but at the cost of long-term stability.

I'm assuming your application is big enough that you don't want to switch to anything else. Sinatra, for instance, doesn't change much and would be a much more stable option.

If you're sticking with Rails, I'd recommend getting rid of as many gem dependencies as possible. There is always a danger that they will no longer be developed or that they could introduce bugs or vulnerabilities.

Also, as much as possible, it's a good idea to favor POROs (plain old Ruby objects) over Rails dependent code. It generally takes more work but you wind up with more stable and reusable code.

I realize that may be more work than you want to put into an application like this, but that's my best advice.

Upvotes: 4

Related Questions