Two-factor authentication through Windows Server 2008 Net Policy Server

Nick Owen of WiKID Systems Inc. offers a step-by-step tutorial to help enterprises add strong authentication to the network.

<meta content="text/html; charset=utf-8" http-equiv="content-type"/><style type="text/css"> <!-- .command { padding: 1em; border: 1px dashed #2f6fab; color: black; background-color: #f9f9f9; line-height: 1.1em; font-family: Courier New, Courier, mono; font-size: 12px; font-style: italic; } .system { color: black; font-family: Courier New, Courier, mono; font-size: 12px; font-style: italic; } .highlight { color: #FF0000; font-family: Georgia, "Times New Roman", Times, serif; font-size: 12px; text-decoration: underline; } --> </style><p>Increasingly, whether due to regulatory requirements or a basic recognition that static passwords just don't provide adequate security, organizations are implementing some form of strong authentication. Like all new efforts, before you start you want to be reasonably assured that you will succeed. In this tutorial we will document how to add two-factor authentication to various Microsoft remote access solutions through the Windows Server 2008 <a href="http://technet.microsoft.com/en-us/network/bb629414.aspx" target="http://technet.microsoft.com/en-us/network/bb629414.aspx" rel="nofollow">Network Policy Server</a>. For two-factor authentication, we will be using the <a href="http://www.wikidsystems.com/?cso01" target="http://www.wikidsystems.com/?cso01" rel="nofollow">WiKID Strong Authentication Server - Enterprise Edition</a>. <a href="http://www.wikidsystems.com" target="http://www.wikidsystems.com" rel="nofollow">WiKID</a> is a dual-sourced, software-based two-factor authentication system. While the document is product specific, the process is typically the same no matter the products.</p><p>Assume that you have a mixed OS environment with some Windows, some Linux/Unix. You have a new requirement for two-factor authentication to meet PCI requirements. You intend to protect all key systems, which are mostly linux and you are going to lock down your remote desktop with two-factor authentication too (though we will only discuss the SSH here). The plan is to create an SSH gateway server that is locked down with two-factor authentication. Admins can then jump from the gateway box to other servers using public key authentication. </p><p>SSH offers a highly secure channel for remote administration of servers. However, since you face an audit for PCI, you have become aware of some potential authentication related short-comings that may cause headaches in an audit. For example: </p><div id="editorialfakesidebardiv" class="fakesidebar fakesidebar-auto"> </div><ul> <li>There is no way to control which users have public key authorization</li> <li>There is no way to enforce passphrase complexity (or even be sure that one is being used)</li> <li>There is no way to expire a public key</li> </ul><p>Additionally, your intention is to add two-factor authentication to other services, such as RDP and a VPN. There is great benefit in having a single two-factor authentication service for all those services and SSH keys will not work for other services.</p><h2>An overview</h2><p>After everything is configured, the system will work like this: The user generates a one-time passcode from their WiKID software token. They enter it into the SSH password field. The credentials are passed from the SSH gateway to NPS via radius. NPS validates that the user is active in AD and in the proper group. If so, it sends the username and one-time password to the WiKID Strong Authentication Server still using Radius. If the OTP is valid, the WiKID server responds to the NPS, which in turn responds to the SSH gateway server and the user is granted access. Note that this process is only for authentication, session management is still handled by the SSH gateway or any other remote access service you are using. </p><h2>First we will enable Windows Server 2008 Network Policy Server (NPS)</h2><p>Add the "Network Policy and Access Services" role to your domain controller.</p><aside class="nativo-promo nativo-promo-1 smartphone" id=""> </aside><p>Enable these role services during installation:</p><div id="sponsoredfakesidebardiv" class="fakesidebar fakesidebar-auto fakesidebar-sponsored"> </div><ul><li>Network Policy Server</li> <li> Routing & Remote Access Services</li> <li>Remote Access Service</li> <li> Routing</li> </ul><h2>Next we add a new RADIUS Client - The SSH Gateway in this case.</h2><p>From Administrative Tools select Network Policy Server</p><aside class="nativo-promo nativo-promo-1 tablet desktop" id=""> </aside><p>Right click on Radius Clients and Select New</p><p> Add a name, the ip address of your remote access server (RAS, VPN, etc) and create a shared secret. You will enter the same shared secret on the WiKID server.</p><p>Click OK</p><h2>Add a new Radius Server - The WiKID Strong Authentication Server</h2><p>Right click on Remote RADIUS servers and name the group, something like "WiKID".</p><aside class="nativo-promo nativo-promo-2 tablet desktop smartphone" id=""> </aside><p>Click the Add button to add a new radius server in the group.</p><p>Enter the IP address of the WiKID server on the first tab. On the second tab, enter the shared secret. That should be all you need to change.</p><h2>Creating a Network Policy</h2><p>Now that we've created the radius client and radius server (WiKID), we need a new Network Policy that tells IAS which users to proxy to WiKID. </p><p>Enter a name and leave Type of network access server as Unspecified or choose your remote access system. </p><p>Click on the Conditions tab. I added a condition for all requests from my server's IP address.</p><p>Click on the Settings Page. Click on Authentication and Select the button for "Forward requests to the following remote RADIUS server group for authentication. Choose WiKID.</p><h2>Configuring the WiKID Strong Authentication Server.</h2><p>Now that we've configured the NPS to proxy authentications, we need to configure WiKID to accept them. See the <a href="http://www.wikidsystems.com/support/wikid-support-center/manual/how-to-install-the-wikid-strong-authentication-server" target="http://www.wikidsystems.com/support/wikid-support-center/manual/how-to-install-the-wikid-strong-authentication-server" rel="nofollow">WiKID installation manual</a> for the details on how to install and configure the WiKID server. Here we're just going to be adding a radius network client for the NPS:</p><p>Log into the WiKIDAdmin web interface.</p><p>Click on the Network Clients tab.</p><p>Click on "Create New Network Client". Give the Network Client a name, specify the IP address, select Radius as the protocol and choose which WiKID Domain to use. (WiKID domains hold the users and specify certain security parameters such as PIN length, the lifetime of the one-time passcodes, max bad PIN/passcode attempts, etc.) </p><p>Click Add</p><p>On the next page, enter the Shared Secret. This is the same secret you entered in NPS above in the second tab of the 'Add Radius Server' step on the NPS. <b>Be sure these match!</b> WiKID support adding radius return attributes at the Network Client level and on a per-user group level, however, that is beyond the scope of this document.</p><p>You will get a notice that the network client has been added. You will need to restart the WiKID server from the command line. This loads the network client into the radius interface and opens the radius ports on the built-in WiKID firewall.</p><pre class="prettyprint"># wikidctl restart</pre><h2>Configuring the SSH Gateway Server</h2><h2>Configure the SSH Gateway</h2><p>Now we will configure the central SSH gateway. This linux box is the gateway/proxy to all the production servers in the farm. It should be locked down tight with no extraneous software or services running on it. It should have an external interface for in-bound connections and an internal interface for internal connections. First, we will configure the gateway box to use WiKID for strong authentication of SSH users.</p><p>Start by installing PAM Radius. The PAM Radius home page is <a href="http://www.freeradius.org/pam_radius_auth/" rel="nofollow"> here</a>.</p><p>Download the tar file (as of this writing <a href="ftp://ftp.freeradius.org/pub/radius/pam_radius-1.3.17.tar.gz" rel="nofollow">1.3.17 was the latest)</a>.</p><p>Run:</p><pre class="prettyprint">$ make</pre><p> Copy the resulting shared library to /lib/security.</p><pre class="prettyprint">$ sudo cp pam_radius_auth.so /lib/security/ </pre><p> Edit /etc/pam.d/sshd to allow Radius authentication.</p><pre class="prettyprint">$ sudo vi /etc/pam.d/sshd</pre><p> <b>N.B.: Distributions of linux have different pam.d file formats.</b> Please check with your distribution for specific suggestions. These instructions work for Fedora/Redhat/Centos.</p><p>Go to the first line of the file, hit the Insert key or the i key and insert this line.</p><pre class="prettyprint">auth        sufficient     /lib/security/pam_radius_auth.so</pre><p> The 'sufficient' tag indicates that if the Radius authentication succeeds then no additional authentication will be required. However, if the Radius authentication fails, a username and password from the system will work. Use 'Required' to require strong authentication.</p><p>Write the file and quit. Hit the Esc key to exit insert mode and type ':wq'</p><p>Edit or create your /etc/raddb/server file.  There is a <a href="http://freeradius.org/pam_radius_auth/pam_radius_auth.conf" rel="nofollow">sample here</a>.</p><pre class="prettyprint">vi /etc/raddb/server </pre><p>Below the line:</p><pre class="prettyprint">127.0.0.1       secret      1</pre><p>Add this line, substituting your routableIPAddress:</p><pre class="prettyprint">routableIPaddress      shared_secret      1</pre><p>The routeableIPaddress is the IP address of your NPS server.</p><p> Edit your /etc/pam.d/sshd file thusly: </p><pre class="prettyprint">#%PAM-1.0 auth sufficient /lib/security/pam_radius_auth.so auth include system-auth account required pam_nologin.so account include system-auth password include system-auth session include system-auth session required pam_loginuid.so </pre><p>Add your WiKID server to the /etc/raddb/server file, using the internal IP Address of the WiKID server and the shared secret you entered in the Network Client creation page:</p><pre class="prettyprint"># server[:port] shared_secret timeout (s) 127.0.0.1 secret 1 xxx.xxx.xxx.xx wikidserver_secret 3 </pre><p>Let's add some security to SSH configuration here too. Open your /etc/ssh/sshd_config (not the nearby ssh_config file). Add these configuration options: </p><pre class="prettyprint">#Protocol 2,1 #Check that only protocol 2 is allowed: Protocol 2 #Disallow root login: PermitRootLogin no #Disallow accounts without passwords: PermitEmptyPasswords no </pre><p>If you want to change the port you can. It won't stop an attacker, but it might cut down on log events caused by script kiddies. This gateway box is now set to use WiKID one-time passwords for SSH authentication. All users have to be registered with the WiKID server and no one can login as root. Before we leave this box, we'll do something that is a little bit different - we will have the users create their RSA private key on the gateway. Once each users is signed into the box with WiKID, have them create their keys:</p><p>class="command">ssh-keygen -t rsa</p><p>In my opinion, passphrases for these keys are redundant. They are here only to create a single sign on functionality into the server farm. Obviously, you must be careful to be sure that users do not have access to other keys. </p><h2>Configure the target servers</h2><p>Obviously, we configure these servers to only accept incoming SSH requests from the gateway. We do this by restricting access on port 22 to our internal addresses. Edit /etc/sysconfig/iptables and add or edit the line for SSH on port 22: </p><pre class="prettyprint">-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp -s 192.168.1.0/24 --dport 22 -j ACCEPT </pre><h2>Running the WiKID Software token</h2><p>Start the WiKID token and select the Domain associated with the SSH Gateway. Then enter the PIN and you will get back the one-time passcode. The OTP is time-bounded, but the time can be set on the WiKID server to whatever you want. </p><figure class="medium"><img data-original="https://images.techhive.com/images/idge/imported/article/cso/2010/05/screenshot9-100255283-orig.jpg" class="lazy" loading="lazy"/></figure><figure class="medium"><img data-original="https://images.techhive.com/images/idge/imported/article/cso/2010/05/screenshot10-100255284-orig.jpg" class="lazy" loading="lazy"/></figure><figure class="medium"><img data-original="https://images.techhive.com/images/idge/imported/article/cso/2010/05/screenshot11-100255285-orig.jpg" class="lazy" loading="lazy"/></figure><p>The user simply enters the one-time passcode when prompted by SSH.</p><p>The token can also be run from the command line, which is quote convenient for SSH:</p><pre class="prettyprint">java -cp jWiKID-3.1.3.jar:jwcl.jar com.wikidsystems.jw.JWcl domainid </pre><p>Were domainid is the 12 digit domain identifier. </p><h2>Conclusion</h2><p>Many organizations are facing increased compliance and regulation. Additionally, environments are becoming more and more heterogenous and the Internet is becoming more and more dangerous. At the same time, users need more access and want to telecommute more, which is good from a disaster recovery perspective. </p><p>While this tutorial has focused on adding two-factor authentication to SSH, an un-stated concept is that you have chosen Radius as a network authentication standard. And it's a good one. Most VPNs, remote desktop systems, web servers and other remote access services support Radius. Now when you want to add a new service with two-factor authentication, all you have to consider is "Does it support Radius?". If it does, then all you need to do is point it to your NPS server. The same holds true if you use Freeradius or some other Radius server.</p><p>For more information on WiKID's two-factor authentication system, <a href="http://www.wikidsystems.com" target="http://www.wikidsystems.com" rel="nofollow">please visit our website</a>.</p> <div class="end-note"> <!-- blx4 #2004 blox4.html --> <div id="" class="blx blxParticleendnote blxM2004 blox4_html blxC51120"><aside> <strong>Next read this</strong> <ul> <li><a href="https://www.csoonline.com/article/3531668/the-10-most-powerful-cybersecurity-companies.html">The 10 most powerful cybersecurity companies </a></li> <li><a href="https://www.csoonline.com/article/3340819/7-cheap-or-free-cybersecurity-training-resources.html">12 cheap or free cybersecurity training resources</a></li> <li><a href="https://www.csoonline.com/article/3538288/5-risk-management-mistakes-cisos-still-make.html">5 risk management mistakes CISOs still make</a></li> <li><a href="https://www.csoonline.com/article/3530230/6-security-metrics-that-matter-and-4-that-don-t.html">6 security metrics that matter – and 4 that don’t</a></li> <li><a href="https://www.csoonline.com/article/3538291/8-video-chat-apps-compared-which-is-best-for-security.html">8 video chat apps compared: Which is best for security?</a></li> <li><a href="https://www.csoonline.com/article/2129955/security-awareness-how-to-rob-a-bank-a-social-engineering-walkthrough.html">How to rob a bank: A social engineering walkthrough</a></li> <li><a href="https://www.csoonline.com/article/3539016/10-ways-to-get-more-from-your-security-budget.html">10 ways to get more from your security budget</a></li> <li><a href="https://www.csoonline.com/article/3541724/cybercrime-in-a-recession-10-things-every-ciso-needs-to-know.html">Cybercrime in a recession: 10 things every CISO needs to know</a></li> <li><a href="https://www.csoonline.com/article/3543217/the-cisos-guide-to-securely-handling-layoffs.html">The CISO's guide to securely handling layoffs</a></li> </ul> </aside> </div> </div> </div> <div class="apart-alt tags"> <span class="related">Related: </span> <ul> <li><a class="edition-link-url primary-cat-url2" href="/category/access-control"><span class="primary-cat-name2">Access Control</span></a></li> <li><a class="edition-link-url primary-cat-url2" href="/category/identity-management"><span class="primary-cat-name2">Identity Management</span></a></li> <li><a class="edition-link-url primary-cat-url2" href="/category/network-security"><span class="primary-cat-name2">Network Security</span></a></li> <li><a class="edition-link-url primary-cat-url2" href="/category/privacy"><span class="primary-cat-name2">Privacy</span></a></li> <li><a class="edition-link-url primary-cat-url2" href="/category/security"><span class="primary-cat-name2">Security</span></a></li> <li><a class="edition-link-url primary-cat-url2" href="/category/authentication"><span class="primary-cat-name2">Authentication</span></a></li> </ul> </div> <script> //set business unit / publisher: US / global is default var contentCopyright = "IDG Communications, Inc."; var contentEdition = ""; // if there's an article locale ID, use that to get the business unit / publisher for copyright (empty string is global) if ( contentEdition != "" || contentEdition != null ) { // get locale slug of article (contentEdition) contentEdition = ""; if (contentEdition === 'us') { contentCopyright = "IDG Communications, Inc."; } if (contentEdition === 'asean') { contentCopyright = "IDG Communications, Inc."; } if (contentEdition === 'mideast' || contentEdition === 'middle-east') { contentCopyright = ""; } if (contentEdition === 'uk') { contentCopyright = "IDG Communications Ltd."; } if (contentEdition === 'au') { contentCopyright = "IDG Communications. ABN 14 001 592 650"; } if (contentEdition === 'nz') { contentCopyright = "IDG Communications. ABN 14 001 592 650"; } if (contentEdition === 'in') { contentCopyright = "IDG Media Private Ltd"; } if (contentEdition === 'nl') { contentCopyright = ""; } } $(document).ready(function() { if (contentCopyright != null && contentCopyright != "") { document.querySelector("span.ccbu").textContent = contentCopyright; } }); </script> <p class="content-copy">Copyright © 2010 <span class="ccbu">IDG Communications, Inc.</span></p> <script> $("body").on('click', 'a[data-product-id]', function() { var e = e || window.event; var dataLayer = window.dataLayer = window.dataLayer || []; var prodName = $(this).attr("data-product-name"); prodName = prodName.replace("\\'","'"); // to counter the effects of over-escaping if ( $(this).parents('.slideshow').length > 0 ) { var productLinkPosition = "Slideshow"; } else if ( $(this).parents('.emo-sb').length > 0 ) { var productLinkPosition = "Product Sidebar"; } else if ( $(this).parents('.at-a-glance.top').length > 0 ) { var productLinkPosition = "AAG Top"; } else if ( $(this).parents('.at-a-glance').length > 0 ) { var productLinkPosition = "AAG Bottom"; } else if ( $(this).parents('.quick-hits.medium').length > 0 ) { var productLinkPosition = "Quick Hit Medium"; } else if ( $(this).parents('.quick-hits').length > 0 ) { var productLinkPosition = "Quick Hit Full"; } else if ( $(this).parents('.product-chart').length > 0 ) { var productLinkPosition = "Product Chart"; } else if ( $(this).parents('.emo-list').length > 0 ) { var productLinkPosition = "Product List"; } else { var productLinkPosition = "Body Text"; } dataLayer.push({ 'event': 'affiliateLink', 'eventCategory': 'Affiliate Link', // Hardcoded, not dynamic 'eventAction': 'Click', // Hardcoded, not dynamic 'eventLabel': ''+e.target+'', // The URL the affiliate link leads to 'productVendor': $(this).attr("data-bkvndr"), // The vendor of the product 'productManufacturer': $(this).attr("data-bkmfr"), // The manufacturer of the product 'productName': prodName, // The name of the product 'productId': $(this).attr("data-product-id"), // The ID of the product 'productLinkPosition': productLinkPosition // location of product link (Product Sidebar, Product Chart, etc.) }); window.permutive.track('AffiliateLinkClick', { category: $(this).attr("data-bkc"), name: prodName, manufacturer: $(this).attr("data-bkmfr"), vendor: $(this).attr("data-bkvndr") }); }); </script> <script> $("#drr-container a:not('[data-product-id]')").on('click',function(e){ var dataLayer = window.dataLayer = window.dataLayer || []; var $clickEl = $(this); var clickUrl = $clickEl.attr("href"); var clickParentsUntilDrr = $clickEl.parentsUntil("#drr-container"); var clickText = $clickEl.text().trim(); var linkLocation; var linkType; var regex = new RegExp("cio.com/|csoonline.com/|computerworld.com/|itnews.com/|itworld.com/|infoworld.com/|networkworld.com/|javaworld.com/|techconnect.com/|techhive.com/|macworld.com/|pcworld.com/|greenbot.com/|gamestar.com/|idginsiderpro.com/|idgesg.net/", "i"); if (clickParentsUntilDrr.hasClass("fakesidebar")) { linkLocation = "FSB"; } else if (clickParentsUntilDrr.hasClass("end-note")) { linkLocation = "End Note"; } else { linkLocation = "Body"; } if (regex.test(clickUrl)) { linkType = "Internal"; } else { linkType = "External"; } dataLayer.push({ 'event': 'editBodyLink', 'eventCategory': 'Editorial Body Links', 'eventAction': linkType, 'eventLabel': linkLocation+" | "+clickText+" | "+clickUrl }); }); </script> <script> $("#drr-top-ad .related-promo-wrapper .title > a").on('click',function(e){ var dataLayer = window.dataLayer = window.dataLayer || []; var $clickEl = $(this); var clickUrl = $clickEl.attr("href"); var clickText = $clickEl.text().trim(); var linkLocation = "Promo"; dataLayer.push({ 'event': 'promoModuleLink', 'eventCategory': 'Promotion module', 'eventAction': 'Click', 'eventLabel': linkLocation+" | "+clickText+" | "+clickUrl }); }); </script> <!-- blx4 #1174 blox4.html --> <div class="article-intercept"> <i class="ss-icon ss-lightbulb"></i> <a href="https://www.csoonline.com/article/3531668/the-10-most-powerful-cybersecurity-companies.html">The 10 most powerful cybersecurity companies</a> </div> <script src="/www/js/video/embedder-jwp.js?v=20200805114218"></script> <script type="text/javascript" src="/www/js/ads/jquery.lazyload-ad.js"></script> <script type="text/javascript"> $(document).ready(function() { $('.articleBloxAd').filter( ":visible" ).each(function(index, item) { var id = $(item).attr('id'); var divClass = $(item).attr('class'); var adString = IDG.GPT.getLazyAdCode(); $(item).replaceWith("<div id=\"" + id + "\" class=\"lazyload_blox_ad " + divClass + "\">" + adString + "</div>"); }); try { $("div.lazyload_blox_ad").lazyLoadAd({ threshold : 0, // You can set threshold on how close to the edge ad should come before it is loaded. Default is 0 (when it is visible). forceLoad : false, // Ad is loaded even if not visible. Default is false. onLoad : false, // Callback function on call ad loading onComplete : false, // Callback function when load is loaded timeout : 1500, // Timeout ad load debug : false, // For debug use : draw colors border depends on load status xray : false // For debug use : display a complete page view with ad placements }) ; } catch (exception){ console.log("error loading lazyload_ad " + exception); } }); </script> <style> @media only screen and (min-width: 60.625em) { article .apart.ad.not-lazy { margin-left: 0; float: right; } } /* this spaces the ads in the right rail */ @media only screen and ( min-width: 48em ) { article #drr-top-ad.epo.cat-narrow #imu2 { margin-top: 500px; /*originally 354px*/ } .topDeals.topper { margin-top: 500px; } } @media only screen and ( min-width: 48em ) and ( max-width: 58.063em ) { article #drr-top-ad.epo.cat-narrow #imu2 { margin-top: 0; } } @media only screen and ( min-width: 60.625em ) { article #drr-top-ad.epo.cat-narrow #imu2 { margin-top: 500px; /*originally 354px*/ } .topDeals.topper { margin-top: 500px; } } @media only screen and ( min-width: 48em ) { article #drr-top-ad.epo.cat-narrow div[id^=imu] { margin-top: 500px; } } @media only screen and ( min-width: 48em ) and ( max-width: 58.063em ) { article #drr-top-ad.epo.cat-narrow div[id^=imu] { margin-top: 0; } } @media only screen and ( min-width: 60.625em ) { article #drr-top-ad.epo.cat-narrow div[id^=imu] { margin-top: 500px; } } </style> <script type="text/javascript"> $(function() { var MOBILE_BREAK = 929; if (typeof $.fn.lazyload === 'undefined' ) { if ('loading' in HTMLImageElement.prototype) { const images = document.querySelectorAll('img[loading="lazy"]'); images.forEach(img => { img.src = img.dataset.original; }); if ($(window).width() <= MOBILE_BREAK) { exeImuMobile(); } else { exeImuDesktop(); } } else { $.getScript('/www/js/jquery/jquery.lazyload.min.js',function(){ if (adBlockStatus === 'false') { $("img.lazy").each(function () { if ($(this).parents('.hero-img').length > 0) { var tempHeight = $("#drr-container").width()*.667; } else { var tempHeight = $(this).width()*.667; } $(this).height(tempHeight); }); $("img.lazy").lazyload({ effect : "fadeIn", threshold: 200, failure_limit:25 }); if ($(window).width() <= MOBILE_BREAK) { exeImuMobile(); } else { exeImuDesktop(); } $("img.lazy").each(function (){ $(this).height(""); }); } }); } } else { if ($(window).width() <= MOBILE_BREAK) { exeImuMobile(); } else { exeImuDesktop(); } } }); function exeImuMobile() { //NOTE: don't include the conditionals for if a site has lazyloaded ads or not - they all do at this point. //define necessary variables var TOP_IMU_HEIGHT = 250, GRAF_HEIGHT = 25, AD_HEIGHT_BUFFER = 350, RIGHT_PIXEL_WINDOW = 300, // this is for not near end of page element DEBUG = false; var placementIndex = [], adPositions = new Array(0,1,2,3); // IMU, IMU, IMU, IMU++ cumulativeHeight = 0, loopCounter = 0, placementTarget = TOP_IMU_HEIGHT + GRAF_HEIGHT; // IMU, IMU, IMU if ($("figure.hero-img").height()) { placementTarget += $("figure.hero-img").height(); } //Right Rail modules in mobile view $( ".techDealsModule ul li:nth-child(2)" ).nextAll( "li" ).addClass( "after" ); $( ".topDealsModule ul li:nth-child(2)").nextAll( "li" ).addClass( "after" ); if ($("#drr-container p").length >= 8) { $(".techDeals,.topDeals").insertAfter( "#drr-container > p:eq(7)"); } else { $("div.techDeals, div.topDeals").hide(); } // Add heights of all elements up through read these next (no longer existws) (which is placed after fourth p tag) var firstModIndex = $("#drr-container > p:eq(3)").index(); $("#drr-container").children().slice(0, firstModIndex).each(function() { placementTarget += $(this).height(); }); // Define first mobile ad here so imu counter shows imu1 first imu2 second, etc. var firstMobileAdHtml = getLazyLoadAdHtml(); // Place Right-rail div containers $("#drr-container").children().each(function(index,value) { //ignore any hidden elements in the body, like the mobile-only "read this next" module if ($(this).is(':visible')) { if (DEBUG) { console.log($(this)); } if (cumulativeHeight >= placementTarget) { if (DEBUG) { console.log("cumulativeHeight >= placementTarget and cumulativeHeight is " + cumulativeHeight + " and placementTarget is " + placementTarget); } var placementDiff = 0; //if ($.inArray(loopCounter, adPositions) != -1 || loopCounter >= 5) { if ($.inArray(loopCounter, adPositions) != -1 || (loopCounter >= 5 && loopCounter < 20) ) { //limiting number of imu placements try { IDG.GPT.addExtIMU(); var adDivString; if (true) { adDivString = getLazyLoadAdHtml(); } else { IDG.GPT.IMUCounter = IDG.GPT.IMUCounter + 1; var slotName = IDG.GPT.getIMUSlotName(), adString = "<div id='" + slotName + "'></div><script>$('#" + slotName + "').responsiveAd({screenSize:'971 1115', scriptTags: []}, false);if (Object.keys(IDG.GPT.companions).length > 0) {IDG.GPT.refreshAd('" + slotName + "');}<\/script>"; adDivString = "<div class='apart ad'>" + adString + "</div>"; consent.ads.queue.push(function(){IDG.GPT.defineGoogleTagSlot(slotName ,[[320,50],[300,250],[300,50]],false,true);}); } placementDiff = applyInsert($(this), adDivString); if (DEBUG) { console.log("Just placed an ad and the placementDiff is: " + placementDiff); } placementTarget = cumulativeHeight + placementDiff + AD_HEIGHT_BUFFER; } catch (e){ console.log("Error: "+e); } }// end inArray() loopCounter++; } // Avoid placing elements too soon due to non-large figures inflating the cumulative height if ($(this).is("figure") && !$(this).is("figure.large")) { cumulativeHeight += GRAF_HEIGHT; } else { cumulativeHeight += $(this).height() + GRAF_HEIGHT; } } }); // end $("#drr-container").children().each() // For mobile only, place ad after second paragraph. (This is imu2.) if (firstMobileAdHtml) { $(firstMobileAdHtml).insertAfter("#drr-container > p:eq(1)"); } $("div.lazyload_ad_article").lazyLoadAd({ threshold : 0, forceLoad : false, // Ad is loaded even if not visible. Default is false. onLoad : false, // Callback function on call ad loading onComplete : false, // Callback function when load is loaded timeout : 1500, // Timeout ad load debug : false, // For debug use : draw colors border depends on load status xray : false // For debug use : display a complete page view with ad placements }); /* * Increments imu counter and generates a 'name' based on count like imu2, imu3, etc. * Returns the html and code script needed for the lazy load ad js. */ function getLazyLoadAdHtml() { try { var adString = IDG.GPT.getLazyAdCode(true); return "<div class=\"apart ad lazyload_ad_article\">" + adString + "</div>"; } catch(e) { console.log("Error: "+e); } } /** * @param jqo Original jquery object target * @param divString The div to be inserted. * @return Difference in height between original placement target and final target. * Checks first 4 elements for an allowable placement (600 pixel window). * If none, place element in first location that does not follow a reject element. */ function applyInsert(jqo, divString) { if (DEBUG) { console.log("applyInsert at top and jqo index is: " + jqo.index()); } for (var i=0; i<=4; i++) { $thisElement = jqo.nextAll().andSelf().slice(i, i+1); if (DEBUG) { console.log("Checking first four and i is: " + i + " and this element index is " + $thisElement.index() ); } if ($thisElement.index() < 0) { break; } if (allowPlacement($thisElement)) { return addElement(jqo, $thisElement, divString); } } if (DEBUG) { console.log("No nearby allows so just place in first spot that is not after reject."); } var numElements = jqo.nextAll().length; var startIndex = jqo.index(); for (var i=startIndex; i<=numElements; i++) { var $element = $("#drr-container").children().eq(i); // This element is eligible when not null, not in placement index, and previous element is not reject if ($element != null && (placementIndex == null || placementIndex.indexOf(i) == -1) && !isReject($element.prev())) { return addElement(jqo, $element, divString); } } if (DEBUG) { console.log("Not going to place element: return 0."); } return 0; } /** * @param jqo Original jquery object * @param allowElement Element that is good placement for module/ad * @param divString The div to be inserted before the good element * @return placementHeightDiff Diff in height between original placement target and current target. * * If element is not too close to the end the insert the div before allowable element. * Add element index to placementIndex to keep track of which elements already have placements */ function addElement(jqo, allowElement, divString) { var offset = allowElement.index() - jqo.index(); if (DEBUG) { console.log("addElement: jqo index is " + jqo.index() + " allowElement index is " + allowElement.index()); } if (elementNotNearEnd(allowElement, RIGHT_PIXEL_WINDOW)) { allowElement.before(divString); if (DEBUG) { console.log("addElement: Adding " + allowElement.index() + " to placementIndex."); } placementIndex.push(allowElement.index()); if (offset == 0) { return 0; } else { return getHeightDifference(jqo,allowElement); } } else { if (DEBUG) { console.log("addElement: Near the end so do NOT add."); } return 0; } } function getHeightDifference(jqo,allowElement) { var offset = allowElement.index() - jqo.index(), height = 0, children = null; if (offset > 0) { children = $("#drr-container").children().slice(jqo.index(), allowElement.index()); } else { children = $("#drr-container").children().slice(allowElement.index(), jqo.index()); } if (children != null) { children.each(function(i) { if (DEBUG) { console.log("About to add this element's height to heigh diff offset"); console.log($(this)); } height += $(this).height() + GRAF_HEIGHT; }); } if (offset < 0) { height *= -1; } if (DEBUG) { console.log("getHeightDifference: offset was " + offset + " and height diff is : " + height); } return height; } function allowPlacement(jqo) { if (jqo.prev() != null && isReject(jqo.prev())) { return false; } return true; } function isReject(jqo) { if (jqo != null) { if (jqo.is('h2') || jqo.is('h3') || jqo.is('h4') || jqo.is('h5')) { if (DEBUG) { console.log("isReject: found header"); } return true; } } return false; } // Returns true if height of all elements after this one is more than 500; false otherwise function elementNotNearEnd(element, pixelWindow) { if (pixelWindow === null) { pixelWindow = 500; } if (element === null) { return false; } var remainingHeight = 0, children = $("#drr-container").children().slice(element.index()); if (children === null) { return false; } children.each(function(i){ remainingHeight += $(this).height(); }); if ( remainingHeight > pixelWindow) { return true; } else { if (DEBUG) { console.log("Element too close to end. Remaining height is: " + remainingHeight + " and window is " + pixelWindow); } return false; } } } // end function exeImuRMobile() function exeImuDesktop() { var DEBUG = false; // use this to get artBodyHeight var heroImgHeight = $('figure.hero-img').outerHeight(true); if (heroImgHeight === null) { heroImgHeight = 0; } // use this to get artBodyHeight var galleryItemHeight = $('figure.thm-gallery').outerHeight(true); if (galleryItemHeight === null) { galleryItemHeight = 0; } // use this to get artBodyHeight var atAglanceTop = $('.at-a-glance.top').height(); if (atAglanceTop === null) { atAglanceTop = 0; } // use this to get artBodyHeight var drrContainerHeight = $('div#drr-container').outerHeight(true); // subtract this from availRRheight var teadsInreadHeight = $('div.teads-inread').height(); if (teadsInreadHeight === null) { teadsInreadHeight = 0; } // subtract this from availRRheight var unrulyAdHeight = $('.unruly_in_article_placement').height(); if (unrulyAdHeight === null) { unrulyAdHeight = 0; } //just in case the in-article ads are picked up... subtract from availRRheight var collapsibleAdHeight = unrulyAdHeight + teadsInreadHeight; // new Deals modules that need to be subtracted from availRRheight var techDealsHeight=0; if ($("#drr-top-ad").children(".techDeals").length>0) { techDealsHeight = 500; } var prodDealsHeight=0; if ($("#drr-top-ad").children(".topDeals").length>0) { prodDealsHeight = 500; } var modulesRRHeight = techDealsHeight + prodDealsHeight; // new available RR height: availRRheight var availRRheight = ( heroImgHeight + galleryItemHeight + atAglanceTop + drrContainerHeight); availRRheight = availRRheight - (collapsibleAdHeight + modulesRRHeight); // if there is a gallery video, remove the amount of space used to push the right rail down to accommodate the video in the RR (CAT-102) if (galleryItemHeight > 0) { if (DEBUG) { console.log("if galleryItemHeight greater than zero subtract 476 from workingRRheight / availRRheight: " + galleryItemHeight); } availRRheight = availRRheight - 476; } var topIMUheight = 0; var topIMUonPageload = false; // this is used when topimu height is acquired on pageload so height is not subtracted from availRRheight twice if ( $("#topimu").length !== 0 ) { topIMUheight = $('#topimu').height(); } if ( $("#gpt-showcase").length !== 0 ) { topIMUheight = $('#gpt-showcase').height(); } if (topIMUheight === 0) { topIMUonPageload = false; } else { topIMUonPageload = true; } availRRheight = availRRheight - topIMUheight; if (DEBUG) { console.log("-----on pageload: topIMUheight = " + topIMUheight + " and topIMUonPageload = " + topIMUonPageload + " typeof: " + typeof topIMUonPageload); } if (DEBUG) { console.log('-----initial available RR height = ' + availRRheight); } // removing topper class that adds space on top of top deals module on short RR heights // topper class is only on PCW, MW, TH top deals module if (availRRheight < 1900) { $("div.topDeals").removeClass("topper"); } var adDivString; var adSlotsSizes = [[300,250]]; // default - heights are updated below var dynamicAd = true; // needed for imu's above imu4 var imuHeight = 0; // for all the IMUs - used in slotRenderEnded event var slotIdTop = ""; // slotRenderEnded for topimu / gpt-showcase var slotId = ""; // used in slotRenderEnded event for IMUs (not topimu) var buffspace = 500; // See above. The default is 390. This comes from the rrSpace property in brands' properties. var heightLimit = 250 + buffspace; // chose this because smallest ad (250) plus buffspace = 640 pixels which is greater than 639 :) // this is used to trigger that particular action (placed ad) only once var triggered_no_times = 0; var counter = 0; // keeps track of number of ads placed var imuArray = []; // Beginning of new code... IDGMPM-17422 googletag.cmd.push(function() { // using slotRenderEnded to get height of newly-placed ad googletag.pubads().addEventListener('slotRenderEnded', function(e) { if (DEBUG) { console.log("-----e.slot.getSlotElementId() = " + e.slot.getSlotElementId()); } // ...For topimu on page load... if ( (e.slot.getSlotElementId() === "topimu" || e.slot.getSlotElementId() === "gpt-showcase") && !e.isEmpty ) { //also ensure it's not an empty ad response slotIdTop = e.slot.getSlotElementId(); imuHeight = e.size[1]; if (imuArray.indexOf(slotIdTop) == -1) { imuArray.push(""+slotIdTop+""); // subtract just-placed topimu height from availRRheight if (topIMUonPageload === false) { availRRheight = availRRheight - imuHeight; if (DEBUG) { console.log("-----topimu: e.slot.getSlotElementId() = " + e.slot.getSlotElementId() + " and availRRheight = " + availRRheight); } } } } // ...For all other IMUs... if ( e.slot.getSlotElementId().indexOf("imu") === 0 && !e.isEmpty ) { slotId = e.slot.getSlotElementId(); imuHeight = e.size[1]; // if array does not contain this slotId, add it to array and subtract height if (imuArray.indexOf(slotId) == -1) { imuArray.push(""+slotId+""); // subtract just-placed ad's height plus 390 buffspace pixels from availRRheight availRRheight = availRRheight - (imuHeight + buffspace); if (DEBUG) { console.log("----inside slotRenderEnded event for IMUs: slotId = --" + slotId + "----calculating availRRheight: " + availRRheight + " subtracting imuHeight = " + imuHeight + " plus " + buffspace + " buffer (buffspace)"); } } counter = triggered_no_times; } // this is for detecting direction of scrolling var lastScrollTop = 0; // do the following while scrolling // debounce listen to the scroll ( >ie9 only ) if (window.addEventListener) { window.addEventListener('scroll', rrdebounce(function(event) { var st = window.pageYOffset || document.documentElement.scrollTop; // Credits: "https://github.com/qeremy/so/blob/master/so.dom.js#L426" if (st > lastScrollTop) { var y_scroll_pos = window.pageYOffset; // get offset from scrolling //var imuDivOffset = $("#"+slotIdTop).offset().top; // get offset of topimu on scroll var imuDivOffset = 0; // getting top offset for topimu... using this to trigger placement of imu2 - only happens once if ( (slotIdTop === 'topimu' || slotIdTop === 'gpt-showcase') && triggered_no_times == 0) { imuDivOffset = $("#"+slotIdTop).offset().top; } else { // need to get this top offset after each ad is placed - occurs for each ad placed after the topimu if (slotId.indexOf("imu") === 0 && triggered_no_times > 0) { imuDivOffset = $("#"+slotId).offset().top; } } // if scroll position is greater than the just-placed imu's top offset means we've hit/passed the top of the imu, display next imu do this only once per ad if (y_scroll_pos > imuDivOffset && availRRheight > heightLimit) { // this block creates ad string and appends ad to #drr-top-ad div try { // THIS DETERMINES AD SLOT SIZES BASED ON AVAIL RR HEIGHT if (availRRheight > 999) { // if availRRheight is equal to or greater than 1000 pixels, place ad either 250 or 600 pixels tall adSlotsSizes = [[300,250],[300,600],[120,600],[160,600]]; } else { // if availRRheight is less than 1000 pixels and greater than or equal to 650 pixels tall... if (availRRheight < 1000 && availRRheight > 649) { adSlotsSizes = [[300,250]]; } } // this needs to be equal. if triggered is more than counter, it will place all the ads on the page if (triggered_no_times === counter) { // THIS CREATES AD CODE STRING IDG.GPT.IMUCounter = IDG.GPT.IMUCounter + 1; // push to consent queue if consent given consent.ads.queue.push(function() { var slotName = IDG.GPT.getIMUSlotName(), adString = "<div id='" + slotName + "'></div><script>$('#" + slotName + "').responsiveAd({screenSize:'971 1115', scriptTags: []}, "+dynamicAd+");if (Object.keys(IDG.GPT.companions).length > 0 || IDG.GPT.disableInitialLoad) {IDG.GPT.refreshAd('" + slotName + "');}<\/script>"; adDivString = "<div class='apart ad not-lazy'>" + adString + "</div>"; IDG.GPT.defineGoogleTagSlot(slotName,adSlotsSizes,false,true); // PLACE AD CODE STRING $(adDivString).appendTo('#drr-top-ad'); if (DEBUG) { console.log("********PLACED AD CODE: "+ slotName+" ********"); } }); } // this is so ads are placed one at a time, after ad height and buffer space is subtracted from availRRheight in slotRenderEnded event listener triggered_no_times = counter + 1; } catch (e) { console.log("Error: "+ e); } } // end if y_scroll_pos > imuDivOffset //console.log(".............scrolling down......."); } // end if st > lastScrollTop for detection scroll direction else { //console.log("...........scrolling up.............."); } lastScrollTop = st <= 0 ? 0 : st; // For Mobile or negative scrolling }, 5)); // end scroll / rrdebounce... run maximum of one time every 5ms } // end window.addeventlistener test // end of code for figuring out how many ads to place });// end eventListener slotRenderEnded }); // rate-limits certain functions, handy for attaching to scroll events, for instance function rrdebounce(func, wait, immediate){ var timeout; return function(){ var context = this, args = arguments; var later = function(){ timeout = null; if (!immediate) func.apply(context, args); }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; } } // end function executeDRRDesktop() </script> <script> // this debouce() function is used here and with related-content ribbon-ribbon.jsp function debounce(func, wait, immediate){ var timeout; return function(){ var context = this, args = arguments; var later = function(){ timeout = null; if (!immediate) func.apply(context, args); }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; } let galleryNode; // undefined if element is not found let mobileDeviceWidth = 0; let eventDeviceOrientation = "portrait"; if (window.innerWidth > 0) { mobileDeviceWidth = window.innerWidth; } else { mobileDeviceWidth = document.documentElement.clientWidth; } if ($("figure").hasClass("thm-gallery")) { galleryNode = document.querySelector('.thm-gallery .embed-wrapper'); } var supportsOrientationChange = "onorientationchange" in window, orientationEvent = supportsOrientationChange ? "orientationchange" : "resize"; window.addEventListener(orientationEvent, function() { // if device is rotated to landscape orientation if (window.orientation === 90 || window.orientation === -90) { eventDeviceOrientation = "landscape"; } else { eventDeviceOrientation = "portrait"; } }, false); if (typeof galleryNode !== 'undefined') { if (mobileDeviceWidth <= 480) { window.addEventListener('scroll', debounce(function(e) { // if device is rotated to landscape after page load, do not pause and play video! if (eventDeviceOrientation !== "landscape") { if (isVisible(galleryNode)) { jwplayer('bcplayer-gallery').play(); } else { jwplayer('bcplayer-gallery').pause(); } } }, 5)); } } </script> </section><!-- /.bodee --> <script>var suppressEd = false;</script> <script> if (countryCode !== "" && (countryCode === 'in' || countryCode === "africa" || (brandAbbrev === "cio" && countryCode === "nz") || brandAbbrev === "cso" && countryCode === "au" )) { suppressEd = true; $("div#content-recommender").remove(); } </script> <div id="content-recommender"> <div class="OUTBRAIN" data-widget-id="AR_1" data-ob-template="CSOOnline"></div>   </div> <script> var obEdition = edition || countryCode || ''; var widgetId=""; if (obEdition === "asean") { widgetId="AR_3"; if ($("body").hasClass("slideshow")) { widgetId="AR_4"; } } if (widgetId !== "") { $(".OUTBRAIN").attr("data-widget-id", widgetId); } // suppressed for certain editions if (!suppressEd) { $.getScript("//widgets.outbrain.com/outbrain.js"); } </script> <div class="lazyload_ad"> <code type="text/javascript"> <!-- var slotName = 'bottomleaderboard'; var slotSize = []; if ($thm.deviceClass == 'mobile') { slotSize = [[300,50],[320,50],[300,250]]; } else if ($thm.deviceClass == 'tablet') { slotSize = [[728,90],[468,60]]; } else { slotSize = [[728,90],[970,90],[970,250]]; } IDG.GPT.addDisplayedAd(slotName, "true"); document.write('<div id="' + slotName + '" class="ad-container">'); consent.ads.queue.push(function(){ IDG.GPT.defineGoogleTagSlot(slotName, slotSize, false, true);}); document.write('</div>'); consent.ads.queue.push(function(){ $('#' + slotName).responsiveAd({screenSize:'971 1115', scriptTags: []}, true);}); //--> </code> </div> <link rel="stylesheet" href="/www.idgcsmb/css/tso-links.css?v=20200805114218" /> <div id="tso-wrapper"><div id="tso" style="display:none"></div></div> <script type="text/javascript"> function renderTSO(ads, requireHttps) { $thm.debug("renderTSO:"+ads.length); if (ads && ads.length > 0) { var selectedAds = selectTSOAds(ads,10); if (null != selectedAds && selectedAds.length > 0) { var html = "<h3>Sponsored Links</h3>"; html += "<ul>"; for (var i=0; i<selectedAds.length;i++) { html += "<li>"; html += "<a href='"+selectedAds[i].url+"'> "+selectedAds[i].title+"</a>"; if (null != selectedAds[i].pixel && selectedAds[i].pixel.length > 0) { html += selectedAds[i].pixel; } html += "</li>"; } html += "</ul>"; $("#tso").html(html).show(); } } } function selectTSOAds(ads,max) { if (ads.length <= max) { return ads; } else { var uniq = {}; var found = 0; var selectedAds = []; while (found < max) { var ad = ads[Math.floor(Math.random()*ads.length)]; if (uniq[ad.id] != null) { continue; } else { uniq[ad.id] = true; found++; selectedAds.push(ad); } } return selectedAds; } return null; } </script> </article> </section><!-- /role=main --> </div><!-- /#page-wrapper --> <link rel="stylesheet" href="/www.idge/css/foot.css?v=20200805114218" /> <link rel="stylesheet" href="/www.idge.cso/css/foot.css?v=20200805114218" /> <footer> <section class="brand" itemscope itemtype="http://schema.org/Organization"> <link itemprop="url" href="http://www.csoonline.com"> <a href="/"><span class="logo">CSO Online</span></a> <span class="tagline"> CSO provides news, analysis and research on security and risk management</span> <span class="follow"> <label>Follow us</label> <ul> <li class="lnkdn"><a class="social-media-li-foot" href="http://www.linkedin.com/company/csoonline" itemprop="sameAs" rel="nofollow" target="_blank" onclick="brandFollowTrack('LinkedIn')"><i class="ss-icon ss-social-circle brand ss-linkedin"></i></a></li> <li><a class="social-media-tw-foot" href="https://twitter.com/csoonline" itemprop="sameAs" rel="nofollow" target="_blank" onclick="brandFollowTrack('Twitter')"><i class="ss-icon ss-social-circle ss-twitter"></i></a></li> <li><a class="social-media-fb-foot" href="https://www.facebook.com/CSOonline" itemprop="sameAs" rel="nofollow" target="_blank" onclick="brandFollowTrack('Facebook')"><i class="ss-icon ss-social-circle brand ss-facebook"></i></a></li> <script> if (typeof facebookUrl !== "undefined" && facebookUrl !== null && facebookUrl !== "") { document.querySelector(".social-media-fb-foot").setAttribute("href", facebookUrl); } if (typeof twitterUrl !== "undefined" && twitterUrl !== null && twitterUrl != "") { document.querySelector(".social-media-tw-foot").setAttribute("href", twitterUrl); } if (typeof linkedInUrl !== "undefined" && linkedInUrl !== null && linkedInUrl !== "") { document.querySelector(".social-media-li-foot").setAttribute("href", linkedInUrl); } </script> </ul> </span> </section> <section class="about"> <div class="wrapper"> <nav class="tertiary" id="ft3"> <ul> <li><a class="edition-link-url" href="/about/about.html" >About Us</a> <li><a class="edition-link-url" href="/about/contactus.html" >Contact</a> <li><a class="edition-link-url" href="/about/privacy.html" >Privacy Policy</a> <li><a class="edition-link-url" href="/about/cookie-policy.html" >Cookie Policy</a> <li><a class="edition-link-url" href="/about/member-preferences.html" >Member Preferences</a> <li><a class="edition-link-url" href="/about/contactus.html#advertise-with-us" >Advertising</a> <li><a class="edition-link-url" href="https://www.idg.com/work-here/" target="_blank" rel="nofollow" >IDG Careers</a> <li><a class="edition-link-url" href="/about/adchoices.html" >Ad Choices</a> <li><a class="edition-link-url" href="/about/affiliates.html" >E-commerce Links</a> <li><a class="edition-link-url" href="/about/ccpa.html" >California: Do Not Sell My Personal Info</a> </ul> </nav> </div> </section> <section class="copyright"> <div class="wrapper"> <img src="https://alt.idgesg.net/images/logos/logo-footer-white.png" alt="IDG Communications" /> <p><a href="/about/tos.html">Copyright</a> © 2020 <span class="bu">IDG Communications, Inc.</span></p> <div class="network"> <div id="network-selector"> <div class="label">Explore the IDG Network <i class="ss-icon tick">descend</i></div> <ul> <li><a href="https://www.cio.com" target="_blank" rel="nofollow">CIO</a></li> <li><a href="https://www.computerworld.com" target="_blank" rel="nofollow">Computerworld</a></li> <li><a href="https://www.csoonline.com" target="_blank" rel="nofollow">CSO Online</a></li> <li><a href="https://www.infoworld.com" target="_blank" rel="nofollow">InfoWorld</a></li> <li><a href="https://www.networkworld.com" target="_blank" rel="nofollow">Network World</a></li> </ul> </div><!-- /#network-selector --> </div><!-- /.network --> </div><!-- /.wrapper --> </section> </footer> <script src="/www/js/jquery/jquery-ui.js"></script> <script src="/www/js/jquery/jquery.dfp.min.js"></script> <script src="/www.idge/js/mule/shortstack_nav.js"></script> <div id="mobilewelcomead" class=" ad-container test"> </div> <script type="text/javascript"> consent.ads.queue.push(function(){ IDG.GPT.addDisplayedAd("mobilewelcomead", "true"); $('#mobilewelcomead').responsiveAd({screenSize:'971 1115', scriptTags: []}); IDG.GPT.log("Creating ad: mobilewelcomead - [971 1115]"); }); </script> <script src="/www/js/analytics/tracking.js"></script> <script src="/www.idge/js/jquery/plugins/jquery.colorbox-min.js"></script> <script src="/www.idge/js/article.js?v=20200805114218"></script> <script src="/www.idge/js/jquery/responsive-tables.js"></script> <script src="/www.idge/js/jquery/jquery.tablesorter.min.js"></script> <script> $(document).ready(function() { $("table.tablesorter").tablesorter({ widgets: ['zebra'] }); $("table.tablesorter tbody tr").hover(function() { $(this).toggleClass("selected"); }); $("table.tablesorter thead tr th").each(function(){ if ($(this).find('.ss-icon').length < 1) { $(this).append('<i class="ss-icon"></i>'); } }); }); </script> <script src="/www.idge/js/global.js?v=20200805114218"></script> <script src="/www/js/webfonts/ss-social.js"></script> <script src="/www/js/webfonts/ss-standard.js"></script> <script src="/www/js/analytics/brandAnalytics.js"></script> <script src="/www/js/locales-editions-slug.js?v=20200805114218"></script> <div id="catfish" class=" ad-container test"> </div> <script type="text/javascript"> consent.ads.queue.push(function(){ IDG.GPT.addDisplayedAd("catfish", "true"); $('#catfish').responsiveAd({screenSize:'971 1115', scriptTags: []}); IDG.GPT.log("Creating ad: catfish - [971 1115]"); }); </script> <!-- Begin gpt-skin --> <div id="gpt-skin" class=" ad-container"> </div> <script type="text/javascript"> consent.ads.queue.push(function(){ IDG.GPT.addDisplayedAd("gpt-skin", "true"); IDG.GPT.displayGoogleTagSlot('gpt-skin'); }); </script> <!-- End gpt-skin --> <!-- Include here when empty article and when not empty and article is slideshow as this script is included with DRR code in body-base.jsp. --> <!-- Also do not include on search page with new right rail. OC-1778 --> <script type="text/javascript"> // -- Load Lazy Advertisement placement as deferred $("div.lazyload_ad").lazyLoadAd({ threshold : 0, // You can set threshold on how close to the edge ad should come before it is loaded. Default is 0 (when it is visible). forceLoad : false, // Ad is loaded even if not visible. Default is false. onLoad : false, // Callback function on call ad loading onComplete : false, // Callback function when load is loaded timeout : 1500, // Timeout ad load debug : false, // For debug use : draw colors border depends on load status xray : false // For debug use : display a complete page view with ad placements }) ; </script> <script type='text/javascript'> var ocHead = $("#oc-head").outerHeight(); var relatedContentHeight = $(".related-content-wrapper").outerHeight(); relatedTop = ocHead+50; $(document).ready(function() { // moved debounce fn to body-base.jsp for use with mobile gallery script var lastScrollTop = 0; if (window.addEventListener) { window.addEventListener('scroll', debounce(function(e){ var containerOffset = $("#drr-container p:last").offset().top; var containerHeight = $("#drr-container p:last").outerHeight(); var windowHeight = $(window).height(); var windowScrolltop = $(this).scrollTop(); if (windowScrolltop > lastScrollTop) { // user is scrolling DOWN: hide ribbon $(".related-content-wrapper").css('top','-'+relatedTop+'px'); if ((windowScrolltop > (containerOffset+containerHeight-windowHeight))&& !($('#oc-head').hasClass('no-stick-important'))) { // user is scrolling down and sees last paragraph: show ribbon $(".related-content-wrapper").css('top',ocHead+'px'); } } else { $(".related-content-wrapper").css('top','-'+relatedTop+'px'); } lastScrollTop = windowScrolltop; }, 5)); // run maximum of one time every 5ms } }); g_bRequireHttps = true; var localeId = getLocale(); $.ajax('/ads/tso?localeId=' + localeId,{ dataType: 'json', success: function(data){ renderTSO(data.tsoLinks, g_bRequireHttps); }, error: function(jqXHR,error,thrown){ $thm.debug("TSO AJAX Status: "+error+": "+thrown,true); } }); </script> <!-- Begin comScore Tag --> <script> var _comscore = _comscore || []; _comscore.push( { c1: "2", c2: "6035308", c3: "", c5: "Access Control", c6: "Article: Tip", c15: "2125087" }); (function() { var s = document.createElement("script"), el = document.getElementsByTagName("script")[0]; s.async = true; s.src = (document.location.protocol == "https:" ? "https://sb" : "http://b") + ".scorecardresearch.com/beacon.js"; el.parentNode.insertBefore(s, el); })(); </script> <noscript> <img src="https://sb.scorecardresearch.com/p?c1=2&c2=6035308&cv=2.0&cj=1" /> </noscript> <!-- End comScore Tag --> <script type="text/javascript"> consent.digitalelement.queue.push(function(){ $thm.logPlEvent({"b":"cso","e":"view","t":"article","id":"2125087"}); }); </script> <div id="loginModal"></div> <div id="logoffModal"></div> <script type="text/javascript"> var subscribersSiteId = "eedeabb0-9a59-4b6b-9df3-e55745819adf"; </script> <script type="text/javascript" src="https://cdn.subscribers.com/assets/subscribers.js"></script> <script type="text/javascript"> a=top;f=self; var acceptedDomains = ["lookbookhq.com"]; var cleanedHost = a.location.hostname.split(".").slice(-2).join("."); if(a!=f && acceptedDomains.indexOf(cleanedHost) < 0) { a.location=f.location; } </script> </body> </html>